mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-03 11:07:23 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			301 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			301 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2015 Matthew Holt and The Caddy Authors
 | 
						|
//
 | 
						|
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
// you may not use this file except in compliance with the License.
 | 
						|
// You may obtain a copy of the License at
 | 
						|
//
 | 
						|
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
//
 | 
						|
// Unless required by applicable law or agreed to in writing, software
 | 
						|
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
// See the License for the specific language governing permissions and
 | 
						|
// limitations under the License.
 | 
						|
 | 
						|
package caddyfile
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"unicode"
 | 
						|
 | 
						|
	"golang.org/x/exp/slices"
 | 
						|
)
 | 
						|
 | 
						|
// 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)
 | 
						|
 | 
						|
	type heredocState int
 | 
						|
 | 
						|
	const (
 | 
						|
		heredocClosed  heredocState = 0
 | 
						|
		heredocOpening heredocState = 1
 | 
						|
		heredocOpened  heredocState = 2
 | 
						|
	)
 | 
						|
 | 
						|
	var (
 | 
						|
		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
 | 
						|
 | 
						|
		openBrace        bool // whether current word/token is or started with open curly brace
 | 
						|
		openBraceWritten bool // if openBrace, whether that brace was written or not
 | 
						|
		openBraceSpace   bool // whether there was a non-newline space before open brace
 | 
						|
 | 
						|
		newLines int // count of newlines consumed
 | 
						|
 | 
						|
		comment bool // whether we're in a comment
 | 
						|
		quoted  bool // whether we're in a quoted segment
 | 
						|
		escaped bool // whether current char is escaped
 | 
						|
 | 
						|
		heredoc              heredocState // whether we're in a heredoc
 | 
						|
		heredocEscaped       bool         // whether heredoc is escaped
 | 
						|
		heredocMarker        []rune
 | 
						|
		heredocClosingMarker []rune
 | 
						|
 | 
						|
		nesting int // indentation level
 | 
						|
	)
 | 
						|
 | 
						|
	write := func(ch rune) {
 | 
						|
		out.WriteRune(ch)
 | 
						|
		last = ch
 | 
						|
	}
 | 
						|
 | 
						|
	indent := func() {
 | 
						|
		for tabs := nesting; tabs > 0; tabs-- {
 | 
						|
			write('\t')
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	nextLine := func() {
 | 
						|
		write('\n')
 | 
						|
		beginningOfLine = true
 | 
						|
	}
 | 
						|
 | 
						|
	for {
 | 
						|
		ch, _, err := rdr.ReadRune()
 | 
						|
		if err != nil {
 | 
						|
			if err == io.EOF {
 | 
						|
				break
 | 
						|
			}
 | 
						|
			panic(err)
 | 
						|
		}
 | 
						|
 | 
						|
		// detect whether we have the start of a heredoc
 | 
						|
		if !quoted && !(heredoc != heredocClosed || heredocEscaped) &&
 | 
						|
			space && last == '<' && ch == '<' {
 | 
						|
			write(ch)
 | 
						|
			heredoc = heredocOpening
 | 
						|
			space = false
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if heredoc == heredocOpening {
 | 
						|
			if ch == '\n' {
 | 
						|
				if len(heredocMarker) > 0 && heredocMarkerRegexp.MatchString(string(heredocMarker)) {
 | 
						|
					heredoc = heredocOpened
 | 
						|
				} else {
 | 
						|
					heredocMarker = nil
 | 
						|
					heredoc = heredocClosed
 | 
						|
					nextLine()
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				write(ch)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			if unicode.IsSpace(ch) {
 | 
						|
				// a space means it's just a regular token and not a heredoc
 | 
						|
				heredocMarker = nil
 | 
						|
				heredoc = heredocClosed
 | 
						|
			} else {
 | 
						|
				heredocMarker = append(heredocMarker, ch)
 | 
						|
				if len(heredocMarker) > 32 {
 | 
						|
					errorString := fmt.Sprintf("heredoc marker too long: <<%s", string(heredocMarker))
 | 
						|
					panic(errorString)
 | 
						|
				}
 | 
						|
				write(ch)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
		// if we're in a heredoc, all characters are read&write as-is
 | 
						|
		if heredoc == heredocOpened {
 | 
						|
			write(ch)
 | 
						|
			heredocClosingMarker = append(heredocClosingMarker, ch)
 | 
						|
			if len(heredocClosingMarker) > len(heredocMarker) {
 | 
						|
				heredocClosingMarker = heredocClosingMarker[1:]
 | 
						|
			}
 | 
						|
			// check if we're done
 | 
						|
			if slices.Equal(heredocClosingMarker, heredocMarker) {
 | 
						|
				heredocMarker = nil
 | 
						|
				heredocClosingMarker = nil
 | 
						|
				heredoc = heredocClosed
 | 
						|
			}
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if last == '<' && space {
 | 
						|
			space = false
 | 
						|
		}
 | 
						|
 | 
						|
		if comment {
 | 
						|
			if ch == '\n' {
 | 
						|
				comment = false
 | 
						|
				space = true
 | 
						|
				nextLine()
 | 
						|
				continue
 | 
						|
			} else {
 | 
						|
				write(ch)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if !escaped && ch == '\\' {
 | 
						|
			if space {
 | 
						|
				write(' ')
 | 
						|
				space = false
 | 
						|
			}
 | 
						|
			write(ch)
 | 
						|
			escaped = true
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if escaped {
 | 
						|
			if ch == '<' {
 | 
						|
				heredocEscaped = true
 | 
						|
			}
 | 
						|
			write(ch)
 | 
						|
			escaped = false
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if quoted {
 | 
						|
			if ch == '"' {
 | 
						|
				quoted = false
 | 
						|
			}
 | 
						|
			write(ch)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if space && ch == '"' {
 | 
						|
			quoted = true
 | 
						|
		}
 | 
						|
 | 
						|
		if unicode.IsSpace(ch) {
 | 
						|
			space = true
 | 
						|
			heredocEscaped = false
 | 
						|
			if ch == '\n' {
 | 
						|
				newLines++
 | 
						|
			}
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		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 == '#' {
 | 
						|
			comment = true
 | 
						|
		}
 | 
						|
 | 
						|
		if openBrace && spacePrior && !openBraceWritten {
 | 
						|
			if nesting == 0 && last == '}' {
 | 
						|
				nextLine()
 | 
						|
				nextLine()
 | 
						|
			}
 | 
						|
 | 
						|
			openBrace = false
 | 
						|
			if beginningOfLine {
 | 
						|
				indent()
 | 
						|
			} else if !openBraceSpace {
 | 
						|
				write(' ')
 | 
						|
			}
 | 
						|
			write('{')
 | 
						|
			openBraceWritten = true
 | 
						|
			nextLine()
 | 
						|
			newLines = 0
 | 
						|
			// prevent infinite nesting from ridiculous inputs (issue #4169)
 | 
						|
			if nesting < 10 {
 | 
						|
				nesting++
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		switch {
 | 
						|
		case ch == '{':
 | 
						|
			openBrace = true
 | 
						|
			openBraceWritten = false
 | 
						|
			openBraceSpace = spacePrior && !beginningOfLine
 | 
						|
			if openBraceSpace {
 | 
						|
				write(' ')
 | 
						|
			}
 | 
						|
			continue
 | 
						|
 | 
						|
		case ch == '}' && (spacePrior || !openBrace):
 | 
						|
			if last != '\n' {
 | 
						|
				nextLine()
 | 
						|
			}
 | 
						|
			if nesting > 0 {
 | 
						|
				nesting--
 | 
						|
			}
 | 
						|
			indent()
 | 
						|
			write('}')
 | 
						|
			newLines = 0
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if newLines > 2 {
 | 
						|
			newLines = 2
 | 
						|
		}
 | 
						|
		for i := 0; i < newLines; i++ {
 | 
						|
			nextLine()
 | 
						|
		}
 | 
						|
		newLines = 0
 | 
						|
		if beginningOfLine {
 | 
						|
			indent()
 | 
						|
		}
 | 
						|
		if nesting == 0 && last == '}' && beginningOfLine {
 | 
						|
			nextLine()
 | 
						|
			nextLine()
 | 
						|
		}
 | 
						|
 | 
						|
		if !beginningOfLine && spacePrior {
 | 
						|
			write(' ')
 | 
						|
		}
 | 
						|
 | 
						|
		if openBrace && !openBraceWritten {
 | 
						|
			write('{')
 | 
						|
			openBraceWritten = true
 | 
						|
		}
 | 
						|
 | 
						|
		if spacePrior && ch == '<' {
 | 
						|
			space = true
 | 
						|
		}
 | 
						|
 | 
						|
		write(ch)
 | 
						|
 | 
						|
		beginningOfLine = false
 | 
						|
	}
 | 
						|
 | 
						|
	// 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')
 | 
						|
}
 |