caddyfile: token renderer comments (I1/I3) and continuations (D1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Francis Lavoie
2026-07-14 02:27:16 -04:00
parent 82a5bd828f
commit f45ceb8202
2 changed files with 99 additions and 16 deletions

View File

@@ -16,6 +16,7 @@ package caddyfile
import (
"bytes"
"strings"
)
// Format formats the input Caddyfile to a standard, nice-looking appearance.
@@ -71,7 +72,20 @@ func formatTokens(tokens []Token) []byte {
atLineStart = true
}
// prevWasComment tracks whether the last non-whitespace token emitted was a
// comment. A structural "{" must never fold onto a line whose last token is
// a comment, since "#" runs to end of line and would comment the brace out.
prevWasComment := false
// foldedOpenAt, when >= 0, is the index of a structural "{" that was already
// emitted early (before a trailing comment) as part of the address/comment/
// brace fold; when the loop reaches that index it is skipped.
foldedOpenAt := -1
for i := range tokens {
if i == foldedOpenAt {
// This "{" was already emitted ahead of a trailing comment.
continue
}
tk := tokens[i]
isOpen := isOpenCurlyBrace(tk)
// A structural close brace only closes a block when one is open. When
@@ -80,6 +94,11 @@ func formatTokens(tokens []Token) []byte {
// emitted verbatim at its source position.
isClose := isCloseCurlyBrace(tk) && nesting > 0
// A trailing comment shares its source line with the previous token
// (e.g. after "}", after "{", or after a directive) and renders inline
// on that line; a standalone comment sits alone on its own line.
trailingComment := tk.isComment && wrote && !isNextOnNewLine(tokens[i-1], tk)
// breaks is the number of newlines to emit before this token:
// 0 = stay on the current line, 1 = new line, 2 = one blank line.
breaks := 0
@@ -98,7 +117,13 @@ func formatTokens(tokens []Token) []byte {
case isOpen:
// An opening brace attaches to the current line: never break to a
// new line before it (it joins the preceding token with a space).
breaks = 0
// EXCEPTION: a standalone comment line must keep the following "{"
// on its own line, otherwise the "#" would comment the brace out.
if prevWasComment {
breaks = 1
} else {
breaks = 0
}
case isClose:
// A closing brace always goes on its own line, and dedents.
if wrote && !atLineStart {
@@ -107,21 +132,37 @@ func formatTokens(tokens []Token) []byte {
if nesting > 0 {
nesting--
}
case trailingComment:
// A trailing comment stays on the current line.
breaks = 0
}
// The content following an opening brace always begins on its own
// indented line, regardless of how the source was laid out. This
// applies to any token, including a nested opening brace.
if wrote && isOpenCurlyBrace(tokens[i-1]) && breaks == 0 {
// applies to any token, including a nested opening brace. A trailing
// comment after "{" is the exception: it stays inline on the "{" line.
if wrote && isOpenCurlyBrace(tokens[i-1]) && breaks == 0 && !trailingComment {
breaks = 1
}
// A blank line always follows a top-level closing brace.
if prevTopClose && breaks < 2 {
// A blank line always follows a top-level closing brace, unless this
// token is a comment trailing that brace on the same source line.
if prevTopClose && breaks < 2 && !trailingComment {
breaks = 2
}
prevTopClose = false
// Address/comment/brace fold: when this is a trailing comment whose very
// next token is a structural "{" that opens this line's block, emit the
// "{" on the head line before the comment, so "addr # c"⏎"{" renders as
// "addr { # c". This must happen before any newline is emitted.
if trailingComment && i+1 < len(tokens) && isOpenCurlyBrace(tokens[i+1]) && breaks == 0 && !atLineStart {
out.WriteByte(' ')
out.WriteString(tokens[i+1].Raw())
nesting++
foldedOpenAt = i + 1
}
// Emit the vertical spacing.
for breaks > 0 {
newline()
@@ -133,21 +174,37 @@ func formatTokens(tokens []Token) []byte {
writeIndent()
}
// Emit horizontal separation for same-line tokens: exactly one space
// separates two tokens that share a line (including an attaching "{").
// A structural close brace at nesting zero (an inline literal, see
// above) stays glued to its predecessor when the source had no space.
if !atLineStart {
// A line continuation renders as "\", a newline, then a hanging indent
// of nesting+1 tabs before the token (instead of a single space). The
// token's Raw() carries the source continuation framing ("\"+newline+
// indentation) as a prefix, so it is stripped and re-emitted normalized.
body := tk.Raw()
if tk.continuation && !atLineStart {
out.WriteString(" \\\n")
for j := 0; j < nesting+1; j++ {
out.WriteByte('\t')
}
body = strings.TrimLeft(strings.TrimPrefix(body, "\\"), " \t\r\n")
atLineStart = false
} else if !atLineStart {
// Emit horizontal separation for same-line tokens: exactly one space
// separates two tokens that share a line (including an attaching "{").
// A structural close brace at nesting zero (an inline literal) stays
// glued to its predecessor when the source had no space. A comment
// glues to a preceding quoted/backtick/heredoc token only when no
// space separated them ("x"#c); otherwise it takes a leading space.
inlineClose := isCloseCurlyBrace(tk) && !isClose
if !(inlineClose && !tk.precededBySpace) {
gluedComment := tk.isComment && tokens[i-1].Quoted() && !tk.precededBySpace
if !(inlineClose && !tk.precededBySpace) && !gluedComment {
out.WriteByte(' ')
}
}
// Emit the token body verbatim.
out.WriteString(tk.Raw())
out.WriteString(body)
wrote = true
atLineStart = false
prevWasComment = tk.isComment
// Structural bookkeeping after emitting. Vertical spacing after a
// brace is produced by the next token's break calculation, so we don't

View File

@@ -216,13 +216,11 @@ d {
# g
}
h {
# i
h { # i
}`,
},
{
description: "quotes and escaping",
skip: "task 9: comment placement (I1) keeps a glued trailing comment attached to its token",
input: `"a \"b\" "#c
d
@@ -326,7 +324,6 @@ baz`,
},
{
description: "brace does not fold into comment above",
skip: "task 9: comment placement (I1) keeps a following { off the comment line",
input: `# comment
{
foo
@@ -526,3 +523,32 @@ import ./conf.d/matcher_not_my_subnet.caddy
}
}
}
func TestFormatCommentsOnBraceLines(t *testing.T) {
cases := []struct{ in, want string }{
{"site {\n\tfoo\n} # after close\n", "site {\n\tfoo\n} # after close\n"},
{"site { # note\n\tfoo\n}\n", "site { # note\n\tfoo\n}\n"},
{"site # note\n{\n\tfoo\n}\n", "site { # note\n\tfoo\n}\n"},
}
for _, c := range cases {
if got := string(Format([]byte(c.in))); got != c.want {
t.Errorf("in %q:\n got %q\nwant %q", c.in, got, c.want)
}
}
}
func TestFormatBlankLineCapAfterComment(t *testing.T) {
got := string(Format([]byte("foo # inline\n\n\nbar\n")))
want := "foo # inline\n\nbar\n"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestFormatContinuationHangingIndent(t *testing.T) {
in := "route {\n\treverse_proxy \\\n\ta \\\n\tb\n}\n"
want := "route {\n\treverse_proxy \\\n\t\ta \\\n\t\tb\n}\n"
if got := string(Format([]byte(in))); got != want {
t.Errorf("got %q, want %q", got, want)
}
}