Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
caddyfile: Formatter enhancements
Matthew Holt committed
Mar 25, 2020 at 18:45 UTC
7ee3ab7baa2165990d3fd358878d818154f7ee86
2 files changed
+312
-167
caddyconfig/caddyfile/formatter.go
+154
-89
@@ -20,129 +20,194 @@ import (
20
"unicode"
21
)
22
23
-// Format formats a Caddyfile to conventional standards.
24
-func Format(body []byte) []byte {
25
- reader := bytes.NewReader(body)
26
- result := new(bytes.Buffer)
23
+// Format formats the input Caddyfile to a standard, nice-looking
24
+// appearance. It works by reading each rune of the input and taking
25
+// control over all the bracing and whitespace that is written; otherwise,
26
+// words, comments, placeholders, and escaped characters are all treated
27
+// literally and written as they appear in the input.
28
+func Format(input []byte) []byte {
29
+ input = bytes.TrimSpace(input)
30
+
31
+ out := new(bytes.Buffer)
32
+ rdr := bytes.NewReader(input)
33
34
var (
29
- commented,
30
- quoted,
31
- escaped,
32
- environ,
33
- lineBegin bool
35
+ last rune // the last character that was written to the result
36
+
37
+ space = true // whether current/previous character was whitespace (beginning of input counts as space)
38
+ beginningOfLine = true // whether we are at beginning of line
39
35
- firstIteration = true
40
+ openBrace bool // whether current word/token is or started with open curly brace
41
+ openBraceWritten bool // if openBrace, whether that brace was written or not
42
37
- indentation = 0
43
+ newLines int // count of newlines consumed
44
39
- prev,
40
- curr,
41
- next rune
45
+ comment bool // whether we're in a comment
46
+ quoted bool // whether we're in a quoted segment
47
+ escaped bool // whether current char is escaped
48
43
- err error
49
+ nesting int // indentation level
50
)
51
46
- insertTabs := func(num int) {
47
- for tabs := num; tabs > 0; tabs-- {
48
- result.WriteRune('\t')
49
- }
52
+ write := func(ch rune) {
53
+ out.WriteRune(ch)
54
+ last = ch
55
}
56
52
- for {
53
- prev = curr
54
- curr = next
55
-
56
- if curr < 0 {
57
- break
57
+ indent := func() {
58
+ for tabs := nesting; tabs > 0; tabs-- {
59
+ write('\t')
60
}
61
+ }
62
60
- next, _, err = reader.ReadRune()
63
+ nextLine := func() {
64
+ write('\n')
65
+ beginningOfLine = true
66
+ }
67
+
68
+ for {
69
+ ch, _, err := rdr.ReadRune()
70
if err != nil {
71
if err == io.EOF {
63
- next = -1
72
+ break
73
+ }
74
+ panic(err)
75
+ }
76
+
77
+ if comment {
78
+ if ch == '\n' {
79
+ comment = false
80
} else {
65
- panic(err)
81
+ write(ch)
82
+ continue
83
+ }
84
+ }
85
+
86
+ if !escaped && ch == '\\' {
87
+ if space {
88
+ write(' ')
89
+ space = false
90
}
91
+ write(ch)
92
+ escaped = true
93
+ continue
94
}
95
69
- if firstIteration {
70
- firstIteration = false
71
- lineBegin = true
96
+ if escaped {
97
+ write(ch)
98
+ escaped = false
99
continue
100
}
101
102
if quoted {
76
- if escaped {
77
- escaped = false
78
- } else {
79
- if curr == '\\' {
80
- escaped = true
81
- }
82
- if curr == '"' {
83
- quoted = false
84
- }
85
- }
86
- if curr == '\n' {
103
+ if ch == '"' {
104
quoted = false
105
}
89
- } else if commented {
90
- if curr == '\n' {
91
- commented = false
106
+ write(ch)
107
+ continue
108
+ }
109
+
110
+ if space && ch == '"' {
111
+ quoted = true
112
+ }
113
+
114
+ if unicode.IsSpace(ch) {
115
+ space = true
116
+ if ch == '\n' {
117
+ newLines++
118
}
93
- } else {
94
- if curr == '"' {
95
- quoted = true
119
+ continue
120
+ }
121
+ spacePrior := space
122
+ space = false
123
+
124
+ //////////////////////////////////////////////////////////
125
+ // I find it helpful to think of the formatting loop in two
126
+ // main sections; by the time we reach this point, we
127
+ // know we are in a "regular" part of the file: we know
128
+ // the character is not a space, not in a literal segment
129
+ // like a comment or quoted, it's not escaped, etc.
130
+ //////////////////////////////////////////////////////////
131
+
132
+ if ch == '#' {
133
+ if !spacePrior && !beginningOfLine {
134
+ write(' ')
135
}
97
- if curr == '#' {
98
- commented = true
136
+ comment = true
137
+ }
138
+
139
+ if openBrace && spacePrior && !openBraceWritten {
140
+ if nesting == 0 && last == '}' {
141
+ nextLine()
142
+ nextLine()
143
}
100
- if curr == '}' {
101
- if environ {
102
- environ = false
103
- } else if indentation > 0 {
104
- indentation--
105
- }
144
+
145
+ openBrace = false
146
+ if beginningOfLine {
147
+ indent()
148
+ } else {
149
+ write(' ')
150
}
107
- if curr == '{' {
108
- if unicode.IsSpace(next) {
109
- indentation++
110
-
111
- if !unicode.IsSpace(prev) && !lineBegin {
112
- result.WriteRune(' ')
113
- }
114
- } else {
115
- environ = true
116
- }
151
+ write('{')
152
+ nextLine()
153
+ newLines = 0
154
+ nesting++
155
+ }
156
+
157
+ switch {
158
+ case ch == '{':
159
+ openBrace = true
160
+ openBraceWritten = false
161
+ continue
162
+
163
+ case ch == '}' && (spacePrior || !openBrace):
164
+ if last != '\n' {
165
+ nextLine()
166
}
118
- if lineBegin {
119
- if curr == ' ' || curr == '\t' {
120
- continue
121
- } else {
122
- lineBegin = false
123
- if curr == '{' && unicode.IsSpace(next) {
124
- // If the block is global, i.e., starts with '{'
125
- // One less indentation for these blocks.
126
- insertTabs(indentation - 1)
127
- } else {
128
- insertTabs(indentation)
129
- }
130
- }
131
- } else {
132
- if prev == '{' &&
133
- (curr == ' ' || curr == '\t') &&
134
- (next != '\n' && next != '\r') {
135
- curr = '\n'
136
- }
167
+ if nesting > 0 {
168
+ nesting--
169
}
170
+ indent()
171
+ write('}')
172
+ newLines = 0
173
+ continue
174
}
175
140
- if curr == '\n' {
141
- lineBegin = true
176
+ if newLines > 2 {
177
+ newLines = 2
178
+ }
179
+ for i := 0; i < newLines; i++ {
180
+ nextLine()
181
+ }
182
+ newLines = 0
183
+ if beginningOfLine {
184
+ indent()
185
+ }
186
+ if nesting == 0 && last == '}' {
187
+ nextLine()
188
+ nextLine()
189
}
190
144
- result.WriteRune(curr)
191
+ if !beginningOfLine && spacePrior {
192
+ write(' ')
193
+ }
194
+
195
+ if openBrace && !openBraceWritten {
196
+ if !beginningOfLine {
197
+ write(' ')
198
+ }
199
+ write('{')
200
+ openBraceWritten = true
201
+ }
202
+ write(ch)
203
+
204
+ beginningOfLine = false
205
}
206
147
- return result.Bytes()
207
+ // the Caddyfile does not need any leading or trailing spaces, but...
208
+ trimmedResult := bytes.TrimSpace(out.Bytes())
209
+
210
+ // ...Caddyfiles should, however, end with a newline because
211
+ // newlines are significant to the syntax of the file
212
+ return append(trimmedResult, '\n')
213
}
caddyconfig/caddyfile/formatter_test.go
+158
-78
@@ -15,12 +15,28 @@
15
package caddyfile
16
17
import (
18
+ "strings"
19
"testing"
20
)
21
21
-func TestFormatBasicIndentation(t *testing.T) {
22
- input := []byte(`
23
- a
22
+func TestFormatter(t *testing.T) {
23
+ for i, tc := range []struct {
24
+ description string
25
+ input string
26
+ expect string
27
+ }{
28
+ {
29
+ description: "very simple",
30
+ input: `abc def
31
+ g hi jkl
32
+mn`,
33
+ expect: `abc def
34
+g hi jkl
35
+mn`,
36
+ },
37
+ {
38
+ description: "basic indentation, line breaks, and nesting",
39
+ input: ` a
40
b
41
42
c {
@@ -30,6 +46,8 @@ b
46
e { f
47
}
48
49
+
50
+
51
g {
52
h {
53
i
@@ -44,22 +62,20 @@ l
62
m {
63
n { o
64
}
65
+ p { q r
66
+s }
67
}
68
49
-{
50
- p
51
-}
69
+ {
70
+{ t
71
+ u
72
53
- { q
54
-}
73
+ v
74
56
- {
57
-{ r
58
-}
75
+w
76
}
60
-`)
61
- expected := []byte(`
62
-a
77
+}`,
78
+ expect: `a
79
b
80
81
c {
@@ -86,49 +102,58 @@ m {
102
n {
103
o
104
}
105
+ p {
106
+ q r
107
+ s
108
+ }
109
}
110
111
{
92
- p
93
-}
112
+ {
113
+ t
114
+ u
115
95
-{
96
- q
97
-}
116
+ v
117
99
-{
100
- {
101
- r
118
+ w
119
}
103
-}
104
-`)
105
- testFormat(t, input, expected)
106
-}
107
-
108
-func TestFormatBasicSpacing(t *testing.T) {
109
- input := []byte(`
110
-a{
120
+}`,
121
+ },
122
+ {
123
+ description: "block spacing",
124
+ input: `a{
125
b
126
}
127
128
c{ d
115
-}
116
-`)
117
- expected := []byte(`
118
-a {
129
+}`,
130
+ expect: `a {
131
b
132
}
133
134
c {
135
d
124
-}
125
-`)
126
- testFormat(t, input, expected)
127
-}
128
-
129
-func TestFormatEnvironmentVariable(t *testing.T) {
130
- input := []byte(`
131
-{$A}
136
+}`,
137
+ },
138
+ {
139
+ description: "advanced spacing",
140
+ input: `abc {
141
+ def
142
+}ghi{
143
+ jkl mno
144
+pqr}`,
145
+ expect: `abc {
146
+ def
147
+}
148
+
149
+ghi {
150
+ jkl mno
151
+ pqr
152
+}`,
153
+ },
154
+ {
155
+ description: "env var placeholders",
156
+ input: `{$A}
157
158
b {
159
{$C}
@@ -139,9 +164,8 @@ d { {$E}
164
165
{ {$F}
166
}
142
-`)
143
- expected := []byte(`
144
-{$A}
167
+`,
168
+ expect: `{$A}
169
170
b {
171
{$C}
@@ -153,49 +177,41 @@ d {
177
178
{
179
{$F}
156
-}
157
-`)
158
- testFormat(t, input, expected)
159
-}
160
-
161
-func TestFormatComments(t *testing.T) {
162
- input := []byte(`
163
-# a "\n"
180
+}`,
181
+ },
182
+ {
183
+ description: "comments",
184
+ input: `#a "\n"
185
165
-# b {
186
+ #b {
187
c
188
}
189
190
d {
170
-e # f
191
+e#f
192
# g
193
}
194
195
h { # i
175
-}
176
-`)
177
- expected := []byte(`
178
-# a "\n"
196
+}`,
197
+ expect: `#a "\n"
198
180
-# b {
199
+#b {
200
c
201
}
202
203
d {
185
- e # f
204
+ e #f
205
# g
206
}
207
208
h {
209
# i
191
-}
192
-`)
193
- testFormat(t, input, expected)
194
-}
195
-
196
-func TestFormatQuotesAndEscapes(t *testing.T) {
197
- input := []byte(`
198
-"a \"b\" #c
210
+}`,
211
+ },
212
+ {
213
+ description: "quotes and escaping",
214
+ input: `"a \"b\" "#c
215
d
216
217
e {
@@ -204,9 +220,16 @@ e {
220
221
g { "h"
222
}
207
-`)
208
- expected := []byte(`
209
-"a \"b\" #c
223
+
224
+i {
225
+ "foo
226
+bar"
227
+}
228
+
229
+j {
230
+"\"k\" l m"
231
+}`,
232
+ expect: `"a \"b\" " #c
233
d
234
235
e {
@@ -216,13 +239,70 @@ e {
239
g {
240
"h"
241
}
219
-`)
220
- testFormat(t, input, expected)
242
+
243
+i {
244
+ "foo
245
+bar"
246
}
247
223
-func testFormat(t *testing.T, input, expected []byte) {
224
- output := Format(input)
225
- if string(output) != string(expected) {
226
- t.Errorf("Expected:\n%s\ngot:\n%s", string(expected), string(output))
248
+j {
249
+ "\"k\" l m"
250
+}`,
251
+ },
252
+ {
253
+ description: "bad nesting (too many open)",
254
+ input: `a
255
+{
256
+ {
257
+}`,
258
+ expect: `a {
259
+ {
260
+ }
261
+`,
262
+ },
263
+ {
264
+ description: "bad nesting (too many close)",
265
+ input: `a
266
+{
267
+ {
268
+}}}`,
269
+ expect: `a {
270
+ {
271
+ }
272
+}
273
+}
274
+`,
275
+ },
276
+ {
277
+ description: "json",
278
+ input: `foo
279
+bar "{\"key\":34}"
280
+`,
281
+ expect: `foo
282
+bar "{\"key\":34}"`,
283
+ },
284
+ {
285
+ description: "escaping after spaces",
286
+ input: `foo \"literal\"`,
287
+ expect: `foo \"literal\"`,
288
+ },
289
+ {
290
+ description: "simple placeholders",
291
+ input: `foo {bar}`,
292
+ expect: `foo {bar}`,
293
+ },
294
+ } {
295
+ // the formatter should output a trailing newline,
296
+ // even if the tests aren't written to expect that
297
+ if !strings.HasSuffix(tc.expect, "\n") {
298
+ tc.expect += "\n"
299
+ }
300
+
301
+ actual := Format([]byte(tc.input))
302
+
303
+ if string(actual) != tc.expect {
304
+ t.Errorf("\n[TEST %d: %s]\n====== EXPECTED ======\n%s\n====== ACTUAL ======\n%s^^^^^^^^^^^^^^^^^^^^^",
305
+ i, tc.description, string(tc.expect), string(actual))
306
+ }
307
}
308
}