@marcel91200 / caddy Mirror · read-only / commits / 7ee3ab7b
Fork 0

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 }