diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index 78c6004ca..2a0e3b2f9 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -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 diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index ad1d60169..18ef382f7 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -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) + } +}