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
| 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 | } |