mirror of
https://github.com/caddyserver/caddy.git
synced 2025-07-09 03:04:57 -04:00
caddyfile: Formatter enhancements
This commit is contained in:
parent
ba08833b2a
commit
7ee3ab7baa
@ -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,
|
|
||||||
environ,
|
|
||||||
lineBegin bool
|
|
||||||
|
|
||||||
firstIteration = true
|
space = true // whether current/previous character was whitespace (beginning of input counts as space)
|
||||||
|
beginningOfLine = true // whether we are at beginning of line
|
||||||
|
|
||||||
indentation = 0
|
openBrace bool // whether current word/token is or started with open curly brace
|
||||||
|
openBraceWritten bool // if openBrace, whether that brace was written or not
|
||||||
|
|
||||||
prev,
|
newLines int // count of newlines consumed
|
||||||
curr,
|
|
||||||
next rune
|
|
||||||
|
|
||||||
err error
|
comment bool // whether we're in a comment
|
||||||
|
quoted bool // whether we're in a quoted segment
|
||||||
|
escaped bool // whether current char is escaped
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indent := func() {
|
||||||
|
for tabs := nesting; tabs > 0; tabs-- {
|
||||||
|
write('\t')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextLine := func() {
|
||||||
|
write('\n')
|
||||||
|
beginningOfLine = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
prev = curr
|
ch, _, err := rdr.ReadRune()
|
||||||
curr = next
|
|
||||||
|
|
||||||
if curr < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
next, _, err = reader.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
|
||||||
|
}
|
||||||
|
|
||||||
|
if escaped {
|
||||||
|
write(ch)
|
||||||
|
escaped = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if quoted {
|
if quoted {
|
||||||
if escaped {
|
if ch == '"' {
|
||||||
escaped = false
|
|
||||||
} else {
|
|
||||||
if curr == '\\' {
|
|
||||||
escaped = true
|
|
||||||
}
|
|
||||||
if curr == '"' {
|
|
||||||
quoted = false
|
quoted = false
|
||||||
}
|
}
|
||||||
|
write(ch)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if curr == '\n' {
|
|
||||||
quoted = false
|
if space && ch == '"' {
|
||||||
}
|
|
||||||
} else if commented {
|
|
||||||
if curr == '\n' {
|
|
||||||
commented = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if curr == '"' {
|
|
||||||
quoted = true
|
quoted = true
|
||||||
}
|
}
|
||||||
if curr == '#' {
|
|
||||||
commented = true
|
|
||||||
}
|
|
||||||
if curr == '}' {
|
|
||||||
if environ {
|
|
||||||
environ = false
|
|
||||||
} else if indentation > 0 {
|
|
||||||
indentation--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if curr == '{' {
|
|
||||||
if unicode.IsSpace(next) {
|
|
||||||
indentation++
|
|
||||||
|
|
||||||
if !unicode.IsSpace(prev) && !lineBegin {
|
if unicode.IsSpace(ch) {
|
||||||
result.WriteRune(' ')
|
space = true
|
||||||
|
if ch == '\n' {
|
||||||
|
newLines++
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
environ = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if lineBegin {
|
|
||||||
if curr == ' ' || curr == '\t' {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if prev == '{' &&
|
|
||||||
(curr == ' ' || curr == '\t') &&
|
|
||||||
(next != '\n' && next != '\r') {
|
|
||||||
curr = '\n'
|
|
||||||
}
|
}
|
||||||
|
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(' ')
|
||||||
}
|
}
|
||||||
|
comment = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if curr == '\n' {
|
if openBrace && spacePrior && !openBraceWritten {
|
||||||
lineBegin = true
|
if nesting == 0 && last == '}' {
|
||||||
|
nextLine()
|
||||||
|
nextLine()
|
||||||
}
|
}
|
||||||
|
|
||||||
result.WriteRune(curr)
|
openBrace = false
|
||||||
|
if beginningOfLine {
|
||||||
|
indent()
|
||||||
|
} else {
|
||||||
|
write(' ')
|
||||||
|
}
|
||||||
|
write('{')
|
||||||
|
nextLine()
|
||||||
|
newLines = 0
|
||||||
|
nesting++
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Bytes()
|
switch {
|
||||||
|
case ch == '{':
|
||||||
|
openBrace = true
|
||||||
|
openBraceWritten = false
|
||||||
|
continue
|
||||||
|
|
||||||
|
case ch == '}' && (spacePrior || !openBrace):
|
||||||
|
if last != '\n' {
|
||||||
|
nextLine()
|
||||||
|
}
|
||||||
|
if nesting > 0 {
|
||||||
|
nesting--
|
||||||
|
}
|
||||||
|
indent()
|
||||||
|
write('}')
|
||||||
|
newLines = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if newLines > 2 {
|
||||||
|
newLines = 2
|
||||||
|
}
|
||||||
|
for i := 0; i < newLines; i++ {
|
||||||
|
nextLine()
|
||||||
|
}
|
||||||
|
newLines = 0
|
||||||
|
if beginningOfLine {
|
||||||
|
indent()
|
||||||
|
}
|
||||||
|
if nesting == 0 && last == '}' {
|
||||||
|
nextLine()
|
||||||
|
nextLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !beginningOfLine && spacePrior {
|
||||||
|
write(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
if openBrace && !openBraceWritten {
|
||||||
|
if !beginningOfLine {
|
||||||
|
write(' ')
|
||||||
|
}
|
||||||
|
write('{')
|
||||||
|
openBraceWritten = true
|
||||||
|
}
|
||||||
|
write(ch)
|
||||||
|
|
||||||
|
beginningOfLine = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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')
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}`,
|
||||||
`)
|
expect: `a
|
||||||
expected := []byte(`
|
|
||||||
a
|
|
||||||
b
|
b
|
||||||
|
|
||||||
c {
|
c {
|
||||||
@ -86,49 +102,58 @@ m {
|
|||||||
n {
|
n {
|
||||||
o
|
o
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
q r
|
||||||
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
p
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
q
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
r
|
t
|
||||||
}
|
u
|
||||||
}
|
|
||||||
`)
|
|
||||||
testFormat(t, input, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatBasicSpacing(t *testing.T) {
|
v
|
||||||
input := []byte(`
|
|
||||||
a{
|
w
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 {
|
||||||
|
def
|
||||||
|
}ghi{
|
||||||
|
jkl mno
|
||||||
|
pqr}`,
|
||||||
|
expect: `abc {
|
||||||
|
def
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFormatEnvironmentVariable(t *testing.T) {
|
ghi {
|
||||||
input := []byte(`
|
jkl mno
|
||||||
{$A}
|
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…
x
Reference in New Issue
Block a user