1// Copyright 2015 Light Code Labs, LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package caddyfile
16
17import (
18	"errors"
19	"fmt"
20	"io"
21	"strings"
22)
23
24// Dispenser is a type that dispenses tokens, similarly to a lexer,
25// except that it can do so with some notion of structure and has
26// some really convenient methods.
27type Dispenser struct {
28	filename string
29	tokens   []Token
30	cursor   int
31	nesting  int
32}
33
34// NewDispenser returns a Dispenser, ready to use for parsing the given input.
35func NewDispenser(filename string, input io.Reader) Dispenser {
36	tokens, _ := allTokens(input) // ignoring error because nothing to do with it
37	return Dispenser{
38		filename: filename,
39		tokens:   tokens,
40		cursor:   -1,
41	}
42}
43
44// NewDispenserTokens returns a Dispenser filled with the given tokens.
45func NewDispenserTokens(filename string, tokens []Token) Dispenser {
46	return Dispenser{
47		filename: filename,
48		tokens:   tokens,
49		cursor:   -1,
50	}
51}
52
53// Next loads the next token. Returns true if a token
54// was loaded; false otherwise. If false, all tokens
55// have been consumed.
56func (d *Dispenser) Next() bool {
57	if d.cursor < len(d.tokens)-1 {
58		d.cursor++
59		return true
60	}
61	return false
62}
63
64// NextArg loads the next token if it is on the same
65// line. Returns true if a token was loaded; false
66// otherwise. If false, all tokens on the line have
67// been consumed. It handles imported tokens correctly.
68func (d *Dispenser) NextArg() bool {
69	if d.cursor < 0 {
70		d.cursor++
71		return true
72	}
73	if d.cursor >= len(d.tokens) {
74		return false
75	}
76	if d.cursor < len(d.tokens)-1 &&
77		d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
78		d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
79		d.cursor++
80		return true
81	}
82	return false
83}
84
85// NextLine loads the next token only if it is not on the same
86// line as the current token, and returns true if a token was
87// loaded; false otherwise. If false, there is not another token
88// or it is on the same line. It handles imported tokens correctly.
89func (d *Dispenser) NextLine() bool {
90	if d.cursor < 0 {
91		d.cursor++
92		return true
93	}
94	if d.cursor >= len(d.tokens) {
95		return false
96	}
97	if d.cursor < len(d.tokens)-1 &&
98		(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
99			d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
100		d.cursor++
101		return true
102	}
103	return false
104}
105
106// NextBlock can be used as the condition of a for loop
107// to load the next token as long as it opens a block or
108// is already in a block. It returns true if a token was
109// loaded, or false when the block's closing curly brace
110// was loaded and thus the block ended. Nested blocks are
111// not supported.
112func (d *Dispenser) NextBlock() bool {
113	if d.nesting > 0 {
114		d.Next()
115		if d.Val() == "}" {
116			d.nesting--
117			return false
118		}
119		return true
120	}
121	if !d.NextArg() { // block must open on same line
122		return false
123	}
124	if d.Val() != "{" {
125		d.cursor-- // roll back if not opening brace
126		return false
127	}
128	d.Next()
129	if d.Val() == "}" {
130		// Open and then closed right away
131		return false
132	}
133	d.nesting++
134	return true
135}
136
137// Val gets the text of the current token. If there is no token
138// loaded, it returns empty string.
139func (d *Dispenser) Val() string {
140	if d.cursor < 0 || d.cursor >= len(d.tokens) {
141		return ""
142	}
143	return d.tokens[d.cursor].Text
144}
145
146// Line gets the line number of the current token. If there is no token
147// loaded, it returns 0.
148func (d *Dispenser) Line() int {
149	if d.cursor < 0 || d.cursor >= len(d.tokens) {
150		return 0
151	}
152	return d.tokens[d.cursor].Line
153}
154
155// File gets the filename of the current token. If there is no token loaded,
156// it returns the filename originally given when parsing started.
157func (d *Dispenser) File() string {
158	if d.cursor < 0 || d.cursor >= len(d.tokens) {
159		return d.filename
160	}
161	if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
162		return tokenFilename
163	}
164	return d.filename
165}
166
167// Args is a convenience function that loads the next arguments
168// (tokens on the same line) into an arbitrary number of strings
169// pointed to in targets. If there are fewer tokens available
170// than string pointers, the remaining strings will not be changed
171// and false will be returned. If there were enough tokens available
172// to fill the arguments, then true will be returned.
173func (d *Dispenser) Args(targets ...*string) bool {
174	enough := true
175	for i := 0; i < len(targets); i++ {
176		if !d.NextArg() {
177			enough = false
178			break
179		}
180		*targets[i] = d.Val()
181	}
182	return enough
183}
184
185// RemainingArgs loads any more arguments (tokens on the same line)
186// into a slice and returns them. Open curly brace tokens also indicate
187// the end of arguments, and the curly brace is not included in
188// the return value nor is it loaded.
189func (d *Dispenser) RemainingArgs() []string {
190	var args []string
191
192	for d.NextArg() {
193		if d.Val() == "{" {
194			d.cursor--
195			break
196		}
197		args = append(args, d.Val())
198	}
199
200	return args
201}
202
203// ArgErr returns an argument error, meaning that another
204// argument was expected but not found. In other words,
205// a line break or open curly brace was encountered instead of
206// an argument.
207func (d *Dispenser) ArgErr() error {
208	if d.Val() == "{" {
209		return d.Err("Unexpected token '{', expecting argument")
210	}
211	return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
212}
213
214// SyntaxErr creates a generic syntax error which explains what was
215// found and what was expected.
216func (d *Dispenser) SyntaxErr(expected string) error {
217	msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
218	return errors.New(msg)
219}
220
221// EOFErr returns an error indicating that the dispenser reached
222// the end of the input when searching for the next token.
223func (d *Dispenser) EOFErr() error {
224	return d.Errf("Unexpected EOF")
225}
226
227// Err generates a custom parse-time error with a message of msg.
228func (d *Dispenser) Err(msg string) error {
229	msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
230	return errors.New(msg)
231}
232
233// Errf is like Err, but for formatted error messages
234func (d *Dispenser) Errf(format string, args ...interface{}) error {
235	return d.Err(fmt.Sprintf(format, args...))
236}
237
238// numLineBreaks counts how many line breaks are in the token
239// value given by the token index tknIdx. It returns 0 if the
240// token does not exist or there are no line breaks.
241func (d *Dispenser) numLineBreaks(tknIdx int) int {
242	if tknIdx < 0 || tknIdx >= len(d.tokens) {
243		return 0
244	}
245	return strings.Count(d.tokens[tknIdx].Text, "\n")
246}
247
248// isNewLine determines whether the current token is on a different
249// line (higher line number) than the previous token. It handles imported
250// tokens correctly. If there isn't a previous token, it returns true.
251func (d *Dispenser) isNewLine() bool {
252	if d.cursor < 1 {
253		return true
254	}
255	if d.cursor > len(d.tokens)-1 {
256		return false
257	}
258	return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
259		d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
260}
261