@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 706 lines 27.9 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddyfile
16
17 import (
18 "bytes"
19 "strings"
20 "unicode"
21 )
22
23 const maxIndent = 10
24
25 // Format formats the input Caddyfile to a standard, nice-looking appearance.
26 // It tokenizes the input in format mode and renders the token stream, taking
27 // control over bracing, indentation, and whitespace; token bodies (words,
28 // comments, placeholders, quoted/backtick strings, heredocs, and escaped
29 // characters) are emitted verbatim.
30 func Format(input []byte) []byte {
31 return FormatWithOptions(input, FormatOptions{})
32 }
33
34 // FormatOptions configures optional formatting behavior.
35 type FormatOptions struct {
36 // WrapUnbracedSite, when true, wraps a single unbraced site block in braces
37 // (Phase 3). Default false.
38 WrapUnbracedSite bool
39 }
40
41 // FormatWithOptions formats the input Caddyfile with the given options.
42 func FormatWithOptions(input []byte, opts FormatOptions) []byte {
43 tokens, err := lexFormat(input)
44 if err != nil {
45 // On a lex error, fall back to the trimmed input with a trailing newline;
46 // Format never panics (Invariant 3).
47 return trimmedWithNewline(input)
48 }
49 // Some token shapes cannot be rendered without changing what they lex back
50 // to, which would break idempotency. When one is present, preserve the
51 // trimmed input verbatim instead of re-rendering; this is trivially
52 // idempotent since a second pass hits the same condition.
53 //
54 // The mandatory trailing newline is the subtle case: if appending it changes
55 // how the input tokenizes (an unterminated or escaped quote, or a dangling
56 // escape, that swallows the newline into a token), then no rendering is a
57 // fixed point. Detect that here so Format falls back to the trimmed input;
58 // trimming removes the trailing newline again, so the fallback is stable.
59 if hasUnformattableToken(tokens) || trailingNewlineChangesTokens(input) {
60 return trimmedWithNewline(input)
61 }
62 parseTokens, parseErr := Tokenize(input, "")
63 wrapped := false
64 if opts.WrapUnbracedSite {
65 // Decide whether to wrap on the NORMALIZED token stream, not the raw one.
66 // isSingleUnbracedSite uses source-line gaps (a blank-line gap => ambiguous
67 // multi-site => don't wrap), but formatTokens removes blank lines and folds
68 // dangling braces, so a shape that reads as "not a single site" on the raw
69 // stream can read as a single unbraced site on a second pass over the
70 // already-formatted output. Deciding on the normalized stream makes the
71 // decision stable across passes (idempotent). Once wrapped, the output is
72 // braced, so isSingleUnbracedSite is false on any re-run and there is no
73 // double-wrap.
74 normalized := formatTokens(tokens)
75 normToks, nerr := lexFormat(normalized)
76 if nerr == nil && isSingleUnbracedSite(normToks) {
77 // Prefer the original token positions when they are already
78 // unambiguous. Normalization may insert top-level block spacing that
79 // would become an unwanted interior blank line after wrapping.
80 if isSingleUnbracedSite(tokens) {
81 tokens = wrapUnbracedSite(tokens)
82 } else {
83 tokens = wrapUnbracedSite(normToks)
84 }
85 wrapped = true
86 }
87 }
88 out := formatTokens(tokens)
89
90 // Backstop: formatting must be idempotent and must not change the token
91 // stream (only structural whitespace). Re-lex the output and require both
92 // that its token texts match the input's and that re-rendering it reproduces
93 // it exactly (a fixed point). If some pathological escape/whitespace
94 // combination the checks above did not anticipate slips through — changing a
95 // token or leaving the vertical spacing unstable — fall back to preserving
96 // the trimmed input verbatim. Well-formed input is a fixed point on the first
97 // pass, so this only affects degenerate input. A lex error on the output is
98 // likewise treated as a divergence.
99 reToks, rerr := lexFormat(out)
100 outParseTokens, outParseErr := Tokenize(out, "")
101 parseChanged := !wrapped && parseErr == nil && (outParseErr != nil || !sameTokenTexts(parseTokens, outParseTokens))
102 if rerr != nil || parseChanged || !sameTokenTexts(tokens, reToks) ||
103 !bytes.Equal(out, formatTokens(reToks)) {
104 return trimmedWithNewline(input)
105 }
106 return out
107 }
108
109 // lexFormat captures comments and raw source while only normalizing braces the
110 // parser already recognized as standalone structural tokens. Public Lex options
111 // intentionally do not change ordinary token boundaries.
112 func lexFormat(input []byte) ([]Token, error) {
113 tokens, err := Lex(input, "", LexOptions{Comments: true, Raw: true})
114 if err != nil {
115 return nil, err
116 }
117 for i := range tokens {
118 if isOpenCurlyBrace(tokens[i]) || isCloseCurlyBrace(tokens[i]) {
119 tokens[i].raw = tokens[i].Text
120 tokens[i].continuation = false
121 }
122 }
123 return tokens, nil
124 }
125
126 // isSingleUnbracedSite reports whether the format-mode token stream is exactly
127 // one top-level server block written WITHOUT wrapping braces — i.e. a single
128 // site whose address line is followed directly by directives, with no outer
129 // "{"..."}" around them. This is the shape the (default-off, Phase 3)
130 // braced-wrap feature may wrap in braces, so detection must be conservative:
131 // anything ambiguous, already-braced, or multi-site returns false.
132 //
133 // The analysis is purely structural (no filesystem, no import expansion, no env
134 // substitution). It mirrors the parser's address-boundary logic
135 // (parser.addresses, isSnippet, isNamedRoute in parse.go): the ADDRESS LIST is
136 // the run of leading non-comment tokens up to the first newline, honoring
137 // trailing-comma continuation across lines (a token ending in "," means another
138 // address follows, possibly on the next line).
139 //
140 // It returns true iff:
141 // - the address list is non-empty, and
142 // - the first address token is not a snippet "(...)" or named route "&(...)",
143 // and
144 // - no structural "{" appears on the address line (which would make it an
145 // already-braced site block, or, with no address before it, a
146 // global-options block), and
147 // - top-level directive groups are not separated by an ambiguous blank line.
148 //
149 // Interior directive braces (nesting >= 1, e.g. "reverse_proxy {"..."}") are
150 // allowed and ignored. Comments are ignored for the boundary decision.
151 func isSingleUnbracedSite(tokens []Token) bool {
152 // Collect the leading address list: non-comment tokens up to the first
153 // newline, extending across newlines while the previous address token ends
154 // in a trailing comma (comma-continuation).
155 var (
156 firstAddrIdx = -1 // index of the first address token
157 addrLineEndIdx = -1 // index of the last address token
158 expectAnother bool // previous address token ended in ","
159 )
160 for i := range len(tokens) {
161 tk := tokens[i]
162 if tk.isComment {
163 continue
164 }
165 // A structural "{" on the address line ends the address list AND means
166 // the block is braced (a real site block) or, with no address seen yet,
167 // a global-options block. Either way, not a single unbraced site.
168 if isOpenCurlyBrace(tk) {
169 return false
170 }
171 // If this token begins a new line and we're not waiting for another
172 // comma-continued address, the address list ended on the previous token.
173 if firstAddrIdx >= 0 && !expectAnother && fmtNextOnNewLine(tokens[i-1], tk) {
174 break
175 }
176
177 // This token is part of the address list.
178 if firstAddrIdx < 0 {
179 firstAddrIdx = i
180 }
181 addrLineEndIdx = i
182 expectAnother = strings.HasSuffix(tk.Text, ",")
183 }
184
185 // Zero address tokens: not a site.
186 if firstAddrIdx < 0 {
187 return false
188 }
189 if tokens[firstAddrIdx].Text == "import" {
190 return false
191 }
192
193 // Snippet "(...)" or named route "&(...)" first address: not a site.
194 if firstText := tokens[firstAddrIdx].Text; strings.HasPrefix(firstText, "&(") || strings.HasPrefix(firstText, "(") {
195 return false
196 }
197
198 // Walk the remaining tokens (the block body) tracking structural nesting.
199 // A blank line (a gap of >= 2 source lines) between two top-level token
200 // groups is the ambiguous multi-site shape and disqualifies wrapping. The
201 // renderer itself inserts such a gap after an interior directive block, so a
202 // gap immediately following its closing brace is allowed.
203 nesting := 0
204 prevIdx := -1
205 for j := addrLineEndIdx + 1; j < len(tokens); j++ {
206 tk := tokens[j]
207 if tk.isComment {
208 continue
209 }
210 if nesting == 0 && prevIdx >= 0 {
211 // Blank line at top level between directive groups: ambiguous
212 // multi-site. A gap of two or more lines is a blank line.
213 prev := tokens[prevIdx]
214 if !isCloseCurlyBrace(prev) && tk.Line-(prev.Line+fmtNumLineBreaks(prev)) >= 2 {
215 return false
216 }
217 }
218 switch {
219 case isOpenCurlyBrace(tk):
220 nesting++
221 case isCloseCurlyBrace(tk) && nesting > 0:
222 nesting--
223 }
224 prevIdx = j
225 }
226
227 return true
228 }
229
230 // wrapUnbracedSite transforms a single unbraced site block into a braced one by
231 // inserting a synthetic structural "{" after the address list and appending a
232 // synthetic structural "}" at the end; the renderer (formatTokens) then indents
233 // the body and emits the braces. If the token stream is not a single unbraced
234 // site (per isSingleUnbracedSite), the tokens are returned unchanged.
235 //
236 // The synthetic braces are structural (Text/Raw "{"/"}", wasQuoted 0). The
237 // opening brace's Line is the last address token's Line so it folds onto the
238 // address line; the closing brace's Line follows the last body token so it lands
239 // on its own line. Because the wrapped output is a genuinely braced site,
240 // isSingleUnbracedSite returns false for it, so re-formatting is a fixed point
241 // and does not re-wrap (idempotency), and the FormatWithOptions backstop — which
242 // requires the output to re-lex to the same tokens and re-render identically —
243 // does not fire.
244 func wrapUnbracedSite(tokens []Token) []Token {
245 if !isSingleUnbracedSite(tokens) {
246 return tokens
247 }
248
249 // Recompute the address-list boundary the same way isSingleUnbracedSite does:
250 // the leading run of non-comment tokens up to the first newline, extended
251 // across newlines while the previous address token ends in a trailing comma.
252 addrLineEndIdx := -1
253 firstAddrIdx := -1
254 expectAnother := false
255 for i := range len(tokens) {
256 tk := tokens[i]
257 if tk.isComment {
258 continue
259 }
260 if firstAddrIdx >= 0 && !expectAnother && fmtNextOnNewLine(tokens[i-1], tk) {
261 break
262 }
263 if firstAddrIdx < 0 {
264 firstAddrIdx = i
265 }
266 addrLineEndIdx = i
267 expectAnother = strings.HasSuffix(tk.Text, ",")
268 }
269
270 openBrace := Token{
271 File: tokens[addrLineEndIdx].File,
272 Line: tokens[addrLineEndIdx].Line,
273 Text: "{",
274 raw: "{",
275 }
276 closeBrace := Token{
277 File: tokens[len(tokens)-1].File,
278 Line: tokens[len(tokens)-1].Line + fmtNumLineBreaks(tokens[len(tokens)-1]) + 1,
279 Text: "}",
280 raw: "}",
281 }
282
283 wrapped := make([]Token, 0, len(tokens)+2)
284 wrapped = append(wrapped, tokens[:addrLineEndIdx+1]...)
285 wrapped = append(wrapped, openBrace)
286 wrapped = append(wrapped, tokens[addrLineEndIdx+1:]...)
287 wrapped = append(wrapped, closeBrace)
288 return wrapped
289 }
290
291 // trimmedWithNewline trims surrounding whitespace (except significant leading
292 // whitespace before a backslash) and adds a single trailing newline, in a
293 // freshly allocated buffer. The copy matters:
294 // bytes.TrimSpace returns a subslice that shares input's backing array, so
295 // appending to it can write into the caller's buffer (os.ReadFile hands back a
296 // slice with spare capacity), corrupting the input a caller still holds — for
297 // example the "caddy fmt --diff" comparison of input against output.
298 func trimmedWithNewline(input []byte) []byte {
299 trimmed := bytes.TrimSpace(input)
300 // Leading whitespace can make a backslash a line continuation. Removing
301 // it would change the token stream and make this fallback unstable.
302 if len(trimmed) > 0 && trimmed[0] == '\\' {
303 trimmed = bytes.TrimRightFunc(input, unicode.IsSpace)
304 }
305 out := make([]byte, 0, len(trimmed)+1)
306 out = append(out, trimmed...)
307 return append(out, '\n')
308 }
309
310 // trailingNewlineChangesTokens reports whether appending a newline to input
311 // changes its format-mode token-text sequence. This is true for inputs whose
312 // final token would swallow Format's mandatory trailing newline — an
313 // unterminated or escaped quote/backtick, or a dangling escape at EOF — for
314 // which no rendering can be a fixed point. Inputs already ending in a newline,
315 // and well-formed inputs, are unaffected.
316 func trailingNewlineChangesTokens(input []byte) bool {
317 if n := len(input); n > 0 && input[n-1] == '\n' {
318 return false
319 }
320 a, erra := lexFormat(input)
321 withNL := make([]byte, len(input)+1)
322 copy(withNL, input)
323 withNL[len(input)] = '\n'
324 b, errb := lexFormat(withNL)
325 if erra != nil || errb != nil {
326 return erra != errb
327 }
328 return !sameTokenTexts(a, b)
329 }
330
331 // sameTokenTexts reports whether two token streams have the same sequence of
332 // token texts, ignoring comment tokens (comment whitespace is intentionally
333 // normalized by the formatter).
334 func sameTokenTexts(a, b []Token) bool {
335 ai, bi := 0, 0
336 for {
337 for ai < len(a) && a[ai].isComment {
338 ai++
339 }
340 for bi < len(b) && b[bi].isComment {
341 bi++
342 }
343 if ai >= len(a) || bi >= len(b) {
344 return ai >= len(a) && bi >= len(b)
345 }
346 if a[ai].Text != b[bi].Text {
347 return false
348 }
349 ai++
350 bi++
351 }
352 }
353
354 // hasUnformattableToken reports whether the token stream contains a token whose
355 // rendering cannot be made idempotent, so Format falls back to preserving the
356 // input verbatim. Two shapes qualify:
357 //
358 // - An ambiguous heredoc opener: a regular (unquoted, non-comment) token whose
359 // verbatim source is a valid heredoc opener shape ("<<MARKER") and which is
360 // the last token on its source line. It stays literal in the source only
361 // because trailing space separated it from the newline; the formatter drops
362 // that space, so the rendered output would re-lex as a real heredoc opener
363 // and glue in the following line. (An escaped opener "\<<MARKER" re-lexes as
364 // a regular token and is unaffected, so only a bare "<<MARKER" qualifies.)
365 // - An unterminated quote/backtick: a token whose verbatim source begins with
366 // a quote or backtick but which is not marked Quoted (the closing delimiter
367 // was never seen). It swallowed the rest of the input; rendering it and
368 // appending the mandatory trailing newline changes what it lexes back to.
369 // - A leading line continuation: the first token carries continuation framing
370 // ("\"+newline) even though there is no preceding token to continue from.
371 // Its anchoring Line sits a line before its rendered content, so the
372 // vertical-spacing math for the following token is unstable across passes.
373 func hasUnformattableToken(tokens []Token) bool {
374 for i, tk := range tokens {
375 if tk.isComment {
376 continue
377 }
378 // Leading dangling line continuation on the first non-comment token.
379 if !anyNonCommentBefore(tokens, i) && tk.continuation {
380 return true
381 }
382 // Messy line continuation: continuation framing whose backslash is not
383 // immediately followed by the newline (e.g. "\"+vertical-tab+newline). The
384 // formatter's continuation normalization cannot cleanly strip the stray
385 // whitespace, so the token text shifts on re-lex.
386 if tk.continuation && hasMessyContinuationFraming(tk.Raw()) {
387 return true
388 }
389 raw := tk.Raw()
390 // A non-quoted token whose verbatim source ends in a newline swallowed it:
391 // an unterminated quote/backtick, or an escaped quote ("\\"") that ran to
392 // end-of-input. A newline otherwise terminates a token, so no well-formed
393 // token ends in one. Format emits the source verbatim and then trims
394 // trailing whitespace, so such a token changes on re-lex and no rendering
395 // is a fixed point. This has to be decided from the token itself rather
396 // than from whether appending a newline changes the stream: once the
397 // mandatory trailing newline has been appended it sits inside the
398 // swallowing token, and appending another one no longer changes anything,
399 // so only the token shape still reveals the problem. A lone "\\r" is kept
400 // in raw but does not terminate a token, so it is deliberately not
401 // treated as swallowed; heredoc tokens carry wasQuoted == '<'.
402 if tk.wasQuoted == 0 && strings.HasSuffix(raw, "\n") {
403 return true
404 }
405 if tk.wasQuoted != 0 {
406 continue
407 }
408 // Ambiguous heredoc opener.
409 if !strings.HasPrefix(raw, "<<") || len(raw) <= 2 {
410 continue
411 }
412 marker := raw[2:]
413 if !isHeredocMarker(marker) {
414 continue
415 }
416 if i+1 >= len(tokens) || fmtNextOnNewLine(tk, tokens[i+1]) {
417 return true
418 }
419 }
420 return false
421 }
422
423 // anyNonCommentBefore reports whether any token before index i is not a comment.
424 func anyNonCommentBefore(tokens []Token, i int) bool {
425 for j := range i {
426 if !tokens[j].isComment {
427 return true
428 }
429 }
430 return false
431 }
432
433 // hasMessyContinuationFraming reports whether raw begins (after any leading
434 // whitespace) with a line-continuation backslash that is not immediately
435 // followed by a newline — i.e. there is stray whitespace between the "\" and the
436 // newline (e.g. "\"+vertical-tab+newline). A clean continuation has "\" directly
437 // before the newline.
438 func hasMessyContinuationFraming(raw string) bool {
439 s := strings.TrimLeft(raw, " \t\r\n\v\f")
440 if !strings.HasPrefix(s, `\`) {
441 return false
442 }
443 rest := s[1:]
444 return len(rest) > 0 && rest[0] != '\n' && rest[0] != '\r'
445 }
446
447 // fmtNumLineBreaks returns how many physical line breaks a token spans, for use
448 // in format-mode vertical-spacing decisions. For quoted tokens (double-quote,
449 // backtick, and heredoc) it counts the newlines in the verbatim source (Raw)
450 // rather than deriving the count from the processed Text. Raw is the exact
451 // physical span, so this correctly handles an empty-bodied heredoc ("<<E\nE",
452 // which Token.NumLineBreaks over-counts by always adding two) and a quoted token
453 // whose Raw carries a leading line-continuation newline (e.g. "\\\n`...`", which
454 // Token.NumLineBreaks under-counts); both otherwise place a following token on
455 // the wrong line and break idempotency. Unquoted tokens keep Token.NumLineBreaks
456 // (their Raw may carry "\"+newline continuation framing whose newline belongs to
457 // the token's anchoring Line, not to its span).
458 func fmtNumLineBreaks(t Token) int {
459 if t.wasQuoted != 0 {
460 return strings.Count(t.Raw(), "\n")
461 }
462 return t.NumLineBreaks()
463 }
464
465 // fmtNextOnNewLine mirrors isNextOnNewLine but uses fmtNumLineBreaks so that
466 // empty-bodied heredocs are measured by their true line span in format mode.
467 func fmtNextOnNewLine(t1, t2 Token) bool {
468 if t1.File != t2.File {
469 return true
470 }
471 if len(t1.imports) != len(t2.imports) {
472 return true
473 }
474 for i, im := range t1.imports {
475 if im != t2.imports[i] {
476 return true
477 }
478 }
479 return t1.Line+fmtNumLineBreaks(t1) < t2.Line
480 }
481
482 // formatTokens renders a format-mode token stream (as produced by
483 // Lex(..., LexOptions{Comments: true, Raw: true})) into a normalized
484 // Caddyfile. It controls all indentation and structural whitespace; token
485 // bodies are emitted verbatim via Token.Raw().
486 func formatTokens(tokens []Token) []byte {
487 var out bytes.Buffer
488
489 nesting := 0 // current indentation level
490 atLineStart := true // whether the cursor is at the start of a fresh line
491 wrote := false // whether any non-whitespace has been written yet
492 prevTopClose := false
493
494 writeIndent := func() {
495 for range min(nesting, maxIndent) {
496 out.WriteByte('\t')
497 }
498 }
499
500 newline := func() {
501 out.WriteByte('\n')
502 atLineStart = true
503 }
504
505 // prevWasComment tracks whether the last non-whitespace token emitted was a
506 // comment. A structural "{" must never fold onto a line whose last token is
507 // a comment, since "#" runs to end of line and would comment the brace out.
508 prevWasComment := false
509 // prevWasClose tracks whether the last non-whitespace token emitted was a
510 // structural "}". A token that shared the source line with that "}" (other
511 // than another "}" or a trailing comment) must break to its own indented
512 // line rather than folding after the brace (I4).
513 prevWasClose := false
514 // foldedOpenAt, when >= 0, is the index of a structural "{" that was already
515 // emitted early (before a trailing comment) as part of the address/comment/
516 // brace fold; when the loop reaches that index it is skipped.
517 foldedOpenAt := -1
518 // lineStartsWithImport tracks whether the first token of the current
519 // nesting-zero source line is "import". A standalone "{" that opens a
520 // global-options block immediately after such a line must stay on its own
521 // line instead of folding onto the import line (the "import" directive and a
522 // global-options "{" are unrelated statements). See the import exception in
523 // the isOpen case below.
524 lineStartsWithImport := false
525
526 for i := range tokens {
527 if i == foldedOpenAt {
528 // This "{" was already emitted ahead of a trailing comment.
529 continue
530 }
531 tk := tokens[i]
532 // Track whether the first token of the current nesting-zero source line
533 // is "import". A token starts a new source line when it is the very
534 // first token or is on a later line than its predecessor. This is
535 // computed against the pre-token nesting so that a top-level "import"
536 // line is recognized before any brace on a following line dedents.
537 // Braces are not directives, so a line beginning with "{" or "}" leaves
538 // the tracker untouched: the isOpen import exception below needs to see
539 // the state established by the preceding import line.
540 if !isCloseCurlyBrace(tk) && !isOpenCurlyBrace(tk) {
541 startsNewLine := !wrote || fmtNextOnNewLine(tokens[i-1], tk)
542 if startsNewLine {
543 lineStartsWithImport = nesting == 0 && !tk.isComment && tk.Text == "import"
544 }
545 }
546 isOpen := isOpenCurlyBrace(tk)
547 // A structural close brace only closes a block when one is open. When
548 // nesting is zero there is nothing to close (e.g. a "}" glued after a
549 // backtick/quoted argument), so it is treated as an inline literal and
550 // emitted verbatim at its source position.
551 isClose := isCloseCurlyBrace(tk) && nesting > 0
552
553 // A trailing comment shares its source line with the previous token
554 // (e.g. after "}", after "{", or after a directive) and renders inline
555 // on that line; a standalone comment sits alone on its own line.
556 trailingComment := tk.isComment && wrote && !fmtNextOnNewLine(tokens[i-1], tk)
557
558 // breaks is the number of newlines to emit before this token:
559 // 0 = stay on the current line, 1 = new line, 2 = one blank line.
560 breaks := 0
561 if wrote {
562 prev := tokens[i-1]
563 if fmtNextOnNewLine(prev, tk) {
564 breaks = 1
565 // A gap of two or more lines yields at most one blank line.
566 if tk.Line-(prev.Line+fmtNumLineBreaks(prev)) >= 2 {
567 breaks = 2
568 }
569 }
570 }
571
572 switch {
573 case isOpen:
574 // An opening brace attaches to the current line: never break to a
575 // new line before it (it joins the preceding token with a space).
576 // EXCEPTION 1: a standalone comment line must keep the following "{"
577 // on its own line, otherwise the "#" would comment the brace out.
578 // EXCEPTION 2: a standalone "{" (one that begins a new source line)
579 // that immediately follows a top-level "import" line, with no
580 // intervening blank line, opens an unrelated global-options block
581 // and must stay on its own line rather than folding onto the import.
582 standaloneBrace := wrote && fmtNextOnNewLine(tokens[i-1], tk)
583 if prevWasComment {
584 breaks = 1
585 } else if lineStartsWithImport && nesting == 0 && standaloneBrace {
586 breaks = max(breaks, 1)
587 } else {
588 breaks = 0
589 }
590 case isClose:
591 // A closing brace always goes on its own line, and dedents.
592 if wrote && !atLineStart {
593 breaks = 1
594 }
595 if nesting > 0 {
596 nesting--
597 }
598 case trailingComment:
599 // A trailing comment stays on the current line.
600 breaks = 0
601 }
602
603 // A token that shared the source line with a preceding structural "}"
604 // (other than another "}" or a trailing comment) breaks to its own
605 // indented line instead of folding after the brace (I4).
606 if prevWasClose && breaks == 0 && !isClose && !trailingComment {
607 breaks = 1
608 }
609
610 // The content following an opening brace always begins on its own
611 // indented line, regardless of how the source was laid out. This
612 // applies to any token, including a nested opening brace. A trailing
613 // comment after "{" is the exception: it stays inline on the "{" line.
614 if wrote && isOpenCurlyBrace(tokens[i-1]) && breaks == 0 && !trailingComment {
615 breaks = 1
616 }
617
618 // A blank line always follows a top-level closing brace, unless this
619 // token is a comment trailing that brace on the same source line.
620 if prevTopClose && breaks < 2 && !trailingComment {
621 breaks = 2
622 }
623 prevTopClose = false
624
625 // Address/comment/brace fold: when this is a trailing comment whose very
626 // next token is a structural "{" that opens this line's block, emit the
627 // "{" on the head line before the comment, so "addr # c"⏎"{" renders as
628 // "addr { # c". This must happen before any newline is emitted.
629 // It only applies when the comment trails the block's head line (a
630 // directive/address). If the comment trails an opening "{" (i.e. the
631 // previous token is itself a "{"), the following "{" opens a separate
632 // nested block on its own line and must NOT be folded up onto the comment
633 // line; doing so is non-idempotent (a second pass sees the two "{" already
634 // adjacent and breaks them apart again).
635 if trailingComment && i+1 < len(tokens) && isOpenCurlyBrace(tokens[i+1]) && breaks == 0 && !atLineStart &&
636 !isOpenCurlyBrace(tokens[i-1]) {
637 out.WriteByte(' ')
638 out.WriteString(tokens[i+1].Raw())
639 nesting++
640 foldedOpenAt = i + 1
641 }
642
643 // Emit the vertical spacing.
644 for breaks > 0 {
645 newline()
646 breaks--
647 }
648
649 // Emit indentation at the start of a line.
650 if atLineStart {
651 writeIndent()
652 }
653
654 // A line continuation renders as "\", a newline, then a hanging indent
655 // of nesting+1 tabs before the token (instead of a single space). The
656 // token's Raw() carries the source continuation framing ("\"+newline+
657 // indentation) as a prefix, so it is stripped and re-emitted normalized.
658 body := tk.Raw()
659 if tk.continuation && !atLineStart {
660 out.WriteString(" \\\n")
661 for range min(nesting+1, maxIndent) {
662 out.WriteByte('\t')
663 }
664 body = strings.TrimLeft(strings.TrimPrefix(body, "\\"), " \t\r\n")
665 atLineStart = false
666 } else if !atLineStart {
667 // Emit horizontal separation for same-line tokens: exactly one space
668 // separates two tokens that share a line (including an attaching "{").
669 // A structural close brace at nesting zero (an inline literal) stays
670 // glued to its predecessor when the source had no space. A comment
671 // glues to a preceding quoted/backtick/heredoc token only when no
672 // space separated them ("x"#c); otherwise it takes a leading space.
673 // precededBySpace is only reliable after a quoted/backtick/heredoc
674 // token (the delimiting space of an unquoted predecessor is consumed
675 // by its own scan), so gluing an inline close brace is allowed only
676 // when the previous token is quoted; otherwise the separator must be
677 // preserved so two distinct tokens (e.g. "0" then "}") do not merge.
678 inlineClose := isCloseCurlyBrace(tk) && !isClose && tokens[i-1].Quoted()
679 gluedComment := tk.isComment && tokens[i-1].Quoted() && !tk.precededBySpace
680 if (!inlineClose || tk.precededBySpace) && !gluedComment {
681 out.WriteByte(' ')
682 }
683 }
684
685 // Emit the token body verbatim.
686 out.WriteString(body)
687 wrote = true
688 atLineStart = false
689 prevWasComment = tk.isComment
690 prevWasClose = isClose
691
692 // Structural bookkeeping after emitting. Vertical spacing after a
693 // brace is produced by the next token's break calculation, so we don't
694 // emit trailing newlines here.
695 if isOpen {
696 nesting++
697 } else if isClose && nesting == 0 {
698 prevTopClose = true
699 }
700 }
701
702 // Trim leading/trailing whitespace, then ensure exactly one trailing
703 // newline (newlines are significant to Caddyfile syntax).
704 trimmed := bytes.TrimSpace(out.Bytes())
705 return append(trimmed, '\n')
706 }