From c0273f1f049aa66e4426023d8e43d68fbdbccbb6 Mon Sep 17 00:00:00 2001
From: bbaa <bbaa@bbaasite.cn>
Date: Mon, 22 Jan 2024 10:24:49 +0800
Subject: [PATCH] caddyfile: Add heredoc support to `fmt` command (#6056)

---
 caddyconfig/caddyfile/formatter.go      | 74 +++++++++++++++++++++++++
 caddyconfig/caddyfile/formatter_test.go | 70 +++++++++++++++++++++++
 caddyconfig/caddyfile/lexer.go          |  2 +-
 3 files changed, 145 insertions(+), 1 deletion(-)

diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go
index 82581a3d3..18753c65e 100644
--- a/caddyconfig/caddyfile/formatter.go
+++ b/caddyconfig/caddyfile/formatter.go
@@ -31,6 +31,14 @@ func Format(input []byte) []byte {
 	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
 
@@ -47,6 +55,11 @@ func Format(input []byte) []byte {
 		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
 	)
 
@@ -75,6 +88,58 @@ func Format(input []byte) []byte {
 			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)
+				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 string(heredocClosingMarker) == string(heredocMarker) {
+				heredocMarker = nil
+				heredocClosingMarker = nil
+				heredoc = heredocClosed
+			}
+			continue
+		}
+
+		if last == '<' && space {
+			space = false
+		}
+
 		if comment {
 			if ch == '\n' {
 				comment = false
@@ -98,6 +163,9 @@ func Format(input []byte) []byte {
 		}
 
 		if escaped {
+			if ch == '<' {
+				heredocEscaped = true
+			}
 			write(ch)
 			escaped = false
 			continue
@@ -117,6 +185,7 @@ func Format(input []byte) []byte {
 
 		if unicode.IsSpace(ch) {
 			space = true
+			heredocEscaped = false
 			if ch == '\n' {
 				newLines++
 			}
@@ -205,6 +274,11 @@ func Format(input []byte) []byte {
 			write('{')
 			openBraceWritten = true
 		}
+
+		if spacePrior && ch == '<' {
+			space = true
+		}
+
 		write(ch)
 
 		beginningOfLine = false
diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go
index 8e5b36860..6eec822fe 100644
--- a/caddyconfig/caddyfile/formatter_test.go
+++ b/caddyconfig/caddyfile/formatter_test.go
@@ -362,6 +362,76 @@ block {
 
 block {
 }
+`,
+		},
+		{
+			description: "keep heredoc as-is",
+			input: `block {
+	heredoc <<HEREDOC
+	Here's more than one space       Here's more than one space
+	HEREDOC
+}
+`,
+			expect: `block {
+	heredoc <<HEREDOC
+	Here's more than one space       Here's more than one space
+	HEREDOC
+}
+`,
+		},
+		{
+			description: "Mixing heredoc with regular part",
+			input: `block {
+	heredoc <<HEREDOC
+	Here's more than one space       Here's more than one space
+	HEREDOC
+	respond "More than one space will be eaten"     200
+}
+
+block2 {
+	heredoc <<HEREDOC
+	Here's more than one space       Here's more than one space
+	HEREDOC
+	respond "More than one space will be eaten" 200
+}
+`,
+			expect: `block {
+	heredoc <<HEREDOC
+	Here's more than one space       Here's more than one space
+	HEREDOC
+	respond "More than one space will be eaten" 200
+}
+
+block2 {
+	heredoc <<HEREDOC
+	Here's more than one space       Here's more than one space
+	HEREDOC
+	respond "More than one space will be eaten" 200
+}
+`,
+		},
+		{
+			description: "Heredoc as regular token",
+			input: `block {
+	heredoc <<HEREDOC                                 "More than one space will be eaten"
+}
+`,
+			expect: `block {
+	heredoc <<HEREDOC "More than one space will be eaten"
+}
+`,
+		},
+		{
+			description: "Escape heredoc",
+			input: `block {
+	heredoc \<<HEREDOC
+	respond "More than one space will be eaten"                           200
+}
+`,
+			expect: `block {
+	heredoc \<<HEREDOC
+	respond "More than one space will be eaten" 200
+}
 `,
 		},
 	} {
diff --git a/caddyconfig/caddyfile/lexer.go b/caddyconfig/caddyfile/lexer.go
index e5026738b..763afb6c2 100644
--- a/caddyconfig/caddyfile/lexer.go
+++ b/caddyconfig/caddyfile/lexer.go
@@ -313,7 +313,7 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
 	// iterate over each line and strip the whitespace from the front
 	var out string
 	for lineNum, lineText := range lines[:len(lines)-1] {
-		if lineText == "" {
+		if lineText == "" || lineText == "\r" {
 			out += "\n"
 			continue
 		}