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. 243
      caddyconfig/caddyfile/formatter.go
  2. 236
      caddyconfig/caddyfile/formatter_test.go

243
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 {
break
} }
}
next, _, err = reader.ReadRune() nextLine := func() {
write('\n')
beginningOfLine = true
}
for {
ch, _, err := rdr.ReadRune()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
next = -1 break
}
panic(err)
}
if comment {
if ch == '\n' {
comment = false
} else { } else {
panic(err) write(ch)
continue
}
}
if !escaped && ch == '\\' {
if space {
write(' ')
space = false
} }
write(ch)
escaped = true
continue
} }
if firstIteration { if escaped {
firstIteration = false write(ch)
lineBegin = true escaped = false
continue continue
} }
if quoted { if quoted {
if escaped { if ch == '"' {
escaped = false
} else {
if curr == '\\' {
escaped = true
}
if curr == '"' {
quoted = false
}
}
if curr == '\n' {
quoted = false quoted = false
} }
} else if commented { write(ch)
if curr == '\n' { continue
commented = false }
if space && ch == '"' {
quoted = true
}
if unicode.IsSpace(ch) {
space = true
if ch == '\n' {
newLines++
} }
} else { continue
if curr == '"' { }
quoted = true spacePrior := space
space = false
//////////////////////////////////////////////////////////
// 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(' ')
} }
if curr == '#' { comment = true
commented = true }
if openBrace && spacePrior && !openBraceWritten {
if nesting == 0 && last == '}' {
nextLine()
nextLine()
} }
if curr == '}' {
if environ { openBrace = false
environ = false if beginningOfLine {
} else if indentation > 0 { indent()
indentation-- } else {
} write(' ')
} }
if curr == '{' { write('{')
if unicode.IsSpace(next) { nextLine()
indentation++ newLines = 0
nesting++
if !unicode.IsSpace(prev) && !lineBegin { }
result.WriteRune(' ')
} switch {
} else { case ch == '{':
environ = true openBrace = true
} openBraceWritten = false
continue
case ch == '}' && (spacePrior || !openBrace):
if last != '\n' {
nextLine()
} }
if lineBegin { if nesting > 0 {
if curr == ' ' || curr == '\t' { nesting--
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)
}
}
} else {
if prev == '{' &&
(curr == ' ' || curr == '\t') &&
(next != '\n' && next != '\r') {
curr = '\n'
}
} }
indent()
write('}')
newLines = 0
continue
} }
if curr == '\n' { if newLines > 2 {
lineBegin = true newLines = 2
}
for i := 0; i < newLines; i++ {
nextLine()
}
newLines = 0
if beginningOfLine {
indent()
}
if nesting == 0 && last == '}' {
nextLine()
nextLine()
} }
result.WriteRune(curr) if !beginningOfLine && spacePrior {
write(' ')
}
if openBrace && !openBraceWritten {
if !beginningOfLine {
write(' ')
}
write('{')
openBraceWritten = true
}
write(ch)
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')
} }

236
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 {
} t
u
{ v
q
}
{ w
{
r
} }
} }`,
`) },
testFormat(t, input, expected) {
} description: "block spacing",
input: `a{
func TestFormatBasicSpacing(t *testing.T) {
input := []byte(`
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,49 +177,41 @@ 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
} }
d { d {
e # f e#f
# g # g
} }
h { # i h { # i
} }`,
`) expect: `#a "\n"
expected := []byte(`
# a "\n"
# b { #b {
c c
} }
d { d {
e # f e #f
# g # g
} }
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