|
|
@ -20,129 +20,194 @@ import ( |
|
|
|
"unicode" |
|
|
|
) |
|
|
|
|
|
|
|
// Format formats a Caddyfile to conventional standards.
|
|
|
|
func Format(body []byte) []byte { |
|
|
|
reader := bytes.NewReader(body) |
|
|
|
result := new(bytes.Buffer) |
|
|
|
// Format formats the input Caddyfile to a standard, nice-looking
|
|
|
|
// appearance. It works by reading each rune of the input and taking
|
|
|
|
// control over all the bracing and whitespace that is written; otherwise,
|
|
|
|
// 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 ( |
|
|
|
commented, |
|
|
|
quoted, |
|
|
|
escaped, |
|
|
|
environ, |
|
|
|
lineBegin bool |
|
|
|
last rune // the last character that was written to the result
|
|
|
|
|
|
|
|
space = true // whether current/previous character was whitespace (beginning of input counts as space)
|
|
|
|
beginningOfLine = true // whether we are at beginning of line
|
|
|
|
|
|
|
|
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, |
|
|
|
curr, |
|
|
|
next rune |
|
|
|
comment bool // whether we're in a comment
|
|
|
|
quoted bool // whether we're in a quoted segment
|
|
|
|
escaped bool // whether current char is escaped
|
|
|
|
|
|
|
|
err error |
|
|
|
nesting int // indentation level
|
|
|
|
) |
|
|
|
|
|
|
|
insertTabs := func(num int) { |
|
|
|
for tabs := num; tabs > 0; tabs-- { |
|
|
|
result.WriteRune('\t') |
|
|
|
} |
|
|
|
write := func(ch rune) { |
|
|
|
out.WriteRune(ch) |
|
|
|
last = ch |
|
|
|
} |
|
|
|
|
|
|
|
for { |
|
|
|
prev = curr |
|
|
|
curr = next |
|
|
|
indent := func() { |
|
|
|
for tabs := nesting; tabs > 0; tabs-- { |
|
|
|
write('\t') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if curr < 0 { |
|
|
|
break |
|
|
|
nextLine := func() { |
|
|
|
write('\n') |
|
|
|
beginningOfLine = true |
|
|
|
} |
|
|
|
|
|
|
|
next, _, err = reader.ReadRune() |
|
|
|
for { |
|
|
|
ch, _, err := rdr.ReadRune() |
|
|
|
if err != nil { |
|
|
|
if err == io.EOF { |
|
|
|
next = -1 |
|
|
|
} else { |
|
|
|
break |
|
|
|
} |
|
|
|
panic(err) |
|
|
|
} |
|
|
|
|
|
|
|
if comment { |
|
|
|
if ch == '\n' { |
|
|
|
comment = false |
|
|
|
} else { |
|
|
|
write(ch) |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if firstIteration { |
|
|
|
firstIteration = false |
|
|
|
lineBegin = true |
|
|
|
if !escaped && ch == '\\' { |
|
|
|
if space { |
|
|
|
write(' ') |
|
|
|
space = false |
|
|
|
} |
|
|
|
write(ch) |
|
|
|
escaped = true |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
if quoted { |
|
|
|
if escaped { |
|
|
|
write(ch) |
|
|
|
escaped = false |
|
|
|
} else { |
|
|
|
if curr == '\\' { |
|
|
|
escaped = true |
|
|
|
} |
|
|
|
if curr == '"' { |
|
|
|
quoted = false |
|
|
|
} |
|
|
|
continue |
|
|
|
} |
|
|
|
if curr == '\n' { |
|
|
|
|
|
|
|
if quoted { |
|
|
|
if ch == '"' { |
|
|
|
quoted = false |
|
|
|
} |
|
|
|
} else if commented { |
|
|
|
if curr == '\n' { |
|
|
|
commented = false |
|
|
|
write(ch) |
|
|
|
continue |
|
|
|
} |
|
|
|
} else { |
|
|
|
if curr == '"' { |
|
|
|
|
|
|
|
if space && ch == '"' { |
|
|
|
quoted = true |
|
|
|
} |
|
|
|
if curr == '#' { |
|
|
|
commented = true |
|
|
|
|
|
|
|
if unicode.IsSpace(ch) { |
|
|
|
space = true |
|
|
|
if ch == '\n' { |
|
|
|
newLines++ |
|
|
|
} |
|
|
|
continue |
|
|
|
} |
|
|
|
if curr == '}' { |
|
|
|
if environ { |
|
|
|
environ = false |
|
|
|
} else if indentation > 0 { |
|
|
|
indentation-- |
|
|
|
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 == '{' { |
|
|
|
if unicode.IsSpace(next) { |
|
|
|
indentation++ |
|
|
|
|
|
|
|
if !unicode.IsSpace(prev) && !lineBegin { |
|
|
|
result.WriteRune(' ') |
|
|
|
if openBrace && spacePrior && !openBraceWritten { |
|
|
|
if nesting == 0 && last == '}' { |
|
|
|
nextLine() |
|
|
|
nextLine() |
|
|
|
} |
|
|
|
|
|
|
|
openBrace = false |
|
|
|
if beginningOfLine { |
|
|
|
indent() |
|
|
|
} 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 curr == ' ' || curr == '\t' { |
|
|
|
if nesting > 0 { |
|
|
|
nesting-- |
|
|
|
} |
|
|
|
indent() |
|
|
|
write('}') |
|
|
|
newLines = 0 |
|
|
|
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 { |
|
|
|
if prev == '{' && |
|
|
|
(curr == ' ' || curr == '\t') && |
|
|
|
(next != '\n' && next != '\r') { |
|
|
|
curr = '\n' |
|
|
|
for i := 0; i < newLines; i++ { |
|
|
|
nextLine() |
|
|
|
} |
|
|
|
newLines = 0 |
|
|
|
if beginningOfLine { |
|
|
|
indent() |
|
|
|
} |
|
|
|
if nesting == 0 && last == '}' { |
|
|
|
nextLine() |
|
|
|
nextLine() |
|
|
|
} |
|
|
|
|
|
|
|
if !beginningOfLine && spacePrior { |
|
|
|
write(' ') |
|
|
|
} |
|
|
|
|
|
|
|
if curr == '\n' { |
|
|
|
lineBegin = true |
|
|
|
if openBrace && !openBraceWritten { |
|
|
|
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') |
|
|
|
} |
|
|
|