Browse Source

caddyfile: Formatter enhancements

master
Matthew Holt 5 years ago
parent
commit
7ee3ab7baa
No known key found for this signature in database GPG Key ID: 2A349DD577D586A5
  1. 217
      caddyconfig/caddyfile/formatter.go
  2. 226
      caddyconfig/caddyfile/formatter_test.go

217
caddyconfig/caddyfile/formatter.go

@ -20,129 +20,194 @@ import (
"unicode" "unicode"
) )
// Format formats a Caddyfile to conventional standards. // Format formats the input Caddyfile to a standard, nice-looking
func Format(body []byte) []byte { // appearance. It works by reading each rune of the input and taking
reader := bytes.NewReader(body) // control over all the bracing and whitespace that is written; otherwise,
result := new(bytes.Buffer) // words, comments, placeholders, and escaped characters are all treated
// literally and written as they appear in the input.
func Format(input []byte) []byte {
input = bytes.TrimSpace(input)
out := new(bytes.Buffer)
rdr := bytes.NewReader(input)
var ( var (
commented, last rune // the last character that was written to the result
quoted,
escaped, space = true // whether current/previous character was whitespace (beginning of input counts as space)
environ, beginningOfLine = true // whether we are at beginning of line
lineBegin bool
firstIteration = true openBrace bool // whether current word/token is or started with open curly brace
openBraceWritten bool // if openBrace, whether that brace was written or not
indentation = 0 newLines int // count of newlines consumed
prev, comment bool // whether we're in a comment
curr, quoted bool // whether we're in a quoted segment
next rune escaped bool // whether current char is escaped
err error nesting int // indentation level
) )
insertTabs := func(num int) { write := func(ch rune) {
for tabs := num; tabs > 0; tabs-- { out.WriteRune(ch)
result.WriteRune('\t') last = ch
}
} }
for { indent := func() {
prev = curr for tabs := nesting; tabs > 0; tabs-- {
curr = next write('\t')
}
}
if curr < 0 { nextLine := func() {
break write('\n')
beginningOfLine = true
} }
next, _, err = reader.ReadRune() for {
ch, _, err := rdr.ReadRune()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
next = -1 break
} else { }
panic(err) panic(err)
} }
if comment {
if ch == '\n' {
comment = false
} else {
write(ch)
continue
}
} }
if firstIteration { if !escaped && ch == '\\' {
firstIteration = false if space {
lineBegin = true write(' ')
space = false
}
write(ch)
escaped = true
continue continue
} }
if quoted {
if escaped { if escaped {
write(ch)
escaped = false escaped = false
} else { continue
if curr == '\\' {
escaped = true
}
if curr == '"' {
quoted = false
}
} }
if curr == '\n' {
if quoted {
if ch == '"' {
quoted = false quoted = false
} }
} else if commented { write(ch)
if curr == '\n' { continue
commented = false
} }
} else {
if curr == '"' { if space && ch == '"' {
quoted = true quoted = true
} }
if curr == '#' {
commented = true if unicode.IsSpace(ch) {
space = true
if ch == '\n' {
newLines++
}
continue
} }
if curr == '}' { spacePrior := space
if environ { space = false
environ = false
} else if indentation > 0 { //////////////////////////////////////////////////////////
indentation-- // I find it helpful to think of the formatting loop in two
// main sections; by the time we reach this point, we
// know we are in a "regular" part of the file: we know
// the character is not a space, not in a literal segment
// like a comment or quoted, it's not escaped, etc.
//////////////////////////////////////////////////////////
if ch == '#' {
if !spacePrior && !beginningOfLine {
write(' ')
} }
comment = true
} }
if curr == '{' {
if unicode.IsSpace(next) {
indentation++
if !unicode.IsSpace(prev) && !lineBegin { if openBrace && spacePrior && !openBraceWritten {
result.WriteRune(' ') if nesting == 0 && last == '}' {
nextLine()
nextLine()
} }
openBrace = false
if beginningOfLine {
indent()
} else { } else {
environ = true write(' ')
}
write('{')
nextLine()
newLines = 0
nesting++
} }
switch {
case ch == '{':
openBrace = true
openBraceWritten = false
continue
case ch == '}' && (spacePrior || !openBrace):
if last != '\n' {
nextLine()
} }
if lineBegin { if nesting > 0 {
if curr == ' ' || curr == '\t' { nesting--
}
indent()
write('}')
newLines = 0
continue continue
} else {
lineBegin = false
if curr == '{' && unicode.IsSpace(next) {
// If the block is global, i.e., starts with '{'
// One less indentation for these blocks.
insertTabs(indentation - 1)
} else {
insertTabs(indentation)
} }
if newLines > 2 {
newLines = 2
} }
} else { for i := 0; i < newLines; i++ {
if prev == '{' && nextLine()
(curr == ' ' || curr == '\t') && }
(next != '\n' && next != '\r') { newLines = 0
curr = '\n' if beginningOfLine {
indent()
} }
if nesting == 0 && last == '}' {
nextLine()
nextLine()
} }
if !beginningOfLine && spacePrior {
write(' ')
} }
if curr == '\n' { if openBrace && !openBraceWritten {
lineBegin = true if !beginningOfLine {
write(' ')
}
write('{')
openBraceWritten = true
} }
write(ch)
result.WriteRune(curr) beginningOfLine = false
} }
return result.Bytes() // the Caddyfile does not need any leading or trailing spaces, but...
trimmedResult := bytes.TrimSpace(out.Bytes())
// ...Caddyfiles should, however, end with a newline because
// newlines are significant to the syntax of the file
return append(trimmedResult, '\n')
} }

226
caddyconfig/caddyfile/formatter_test.go

@ -15,12 +15,28 @@
package caddyfile package caddyfile
import ( import (
"strings"
"testing" "testing"
) )
func TestFormatBasicIndentation(t *testing.T) { func TestFormatter(t *testing.T) {
input := []byte(` for i, tc := range []struct {
a description string
input string
expect string
}{
{
description: "very simple",
input: `abc def
g hi jkl
mn`,
expect: `abc def
g hi jkl
mn`,
},
{
description: "basic indentation, line breaks, and nesting",
input: ` a
b b
c { c {
@ -30,6 +46,8 @@ b
e { f e { f
} }
g { g {
h { h {
i i
@ -44,22 +62,20 @@ l
m { m {
n { o n { o
} }
p { q r
s }
} }
{ {
p { t
} u
{ q v
}
{ w
{ r
}
} }
`) }`,
expected := []byte(` expect: `a
a
b b
c { c {
@ -86,49 +102,58 @@ m {
n { n {
o o
} }
p {
q r
s
} }
{
p
} }
{ {
q
}
{ {
{ t
r u
}
} v
`)
testFormat(t, input, expected)
}
func TestFormatBasicSpacing(t *testing.T) { w
input := []byte(` }
a{ }`,
},
{
description: "block spacing",
input: `a{
b b
} }
c{ d c{ d
} }`,
`) expect: `a {
expected := []byte(`
a {
b b
} }
c { c {
d d
} }`,
`) },
testFormat(t, input, expected) {
} description: "advanced spacing",
input: `abc {
func TestFormatEnvironmentVariable(t *testing.T) { def
input := []byte(` }ghi{
{$A} jkl mno
pqr}`,
expect: `abc {
def
}
ghi {
jkl mno
pqr
}`,
},
{
description: "env var placeholders",
input: `{$A}
b { b {
{$C} {$C}
@ -139,9 +164,8 @@ d { {$E}
{ {$F} { {$F}
} }
`) `,
expected := []byte(` expect: `{$A}
{$A}
b { b {
{$C} {$C}
@ -153,14 +177,11 @@ d {
{ {
{$F} {$F}
} }`,
`) },
testFormat(t, input, expected) {
} description: "comments",
input: `#a "\n"
func TestFormatComments(t *testing.T) {
input := []byte(`
# a "\n"
#b { #b {
c c
@ -172,10 +193,8 @@ e # f
} }
h { # i h { # i
} }`,
`) expect: `#a "\n"
expected := []byte(`
# a "\n"
#b { #b {
c c
@ -188,14 +207,11 @@ d {
h { h {
# i # i
} }`,
`) },
testFormat(t, input, expected) {
} description: "quotes and escaping",
input: `"a \"b\" "#c
func TestFormatQuotesAndEscapes(t *testing.T) {
input := []byte(`
"a \"b\" #c
d d
e { e {
@ -204,9 +220,16 @@ e {
g { "h" g { "h"
} }
`)
expected := []byte(` i {
"a \"b\" #c "foo
bar"
}
j {
"\"k\" l m"
}`,
expect: `"a \"b\" " #c
d d
e { e {
@ -216,13 +239,70 @@ e {
g { g {
"h" "h"
} }
`)
testFormat(t, input, expected) i {
"foo
bar"
} }
func testFormat(t *testing.T, input, expected []byte) { j {
output := Format(input) "\"k\" l m"
if string(output) != string(expected) { }`,
t.Errorf("Expected:\n%s\ngot:\n%s", string(expected), string(output)) },
{
description: "bad nesting (too many open)",
input: `a
{
{
}`,
expect: `a {
{
}
`,
},
{
description: "bad nesting (too many close)",
input: `a
{
{
}}}`,
expect: `a {
{
}
}
}
`,
},
{
description: "json",
input: `foo
bar "{\"key\":34}"
`,
expect: `foo
bar "{\"key\":34}"`,
},
{
description: "escaping after spaces",
input: `foo \"literal\"`,
expect: `foo \"literal\"`,
},
{
description: "simple placeholders",
input: `foo {bar}`,
expect: `foo {bar}`,
},
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that
if !strings.HasSuffix(tc.expect, "\n") {
tc.expect += "\n"
}
actual := Format([]byte(tc.input))
if string(actual) != tc.expect {
t.Errorf("\n[TEST %d: %s]\n====== EXPECTED ======\n%s\n====== ACTUAL ======\n%s^^^^^^^^^^^^^^^^^^^^^",
i, tc.description, string(tc.expect), string(actual))
}
} }
} }

Loading…
Cancel
Save