text/template: provide a way to trim leading and trailing space between actions
Borrowing a suggestion from the issue listed below, we modify the lexer to
trim spaces at the beginning (end) of a block of text if the action immediately
before (after) is marked with a minus sign. To avoid parsing/lexing ambiguity,
we require an ASCII space between the minus sign and the rest of the action.
Thus:
{{23 -}}
<
{{- 45}}
produces the output
23<45
All the work is done in the lexer. The modification is invisible to the parser
or any outside package (except I guess for noticing some gaps in the input
if one tracks error positions). Thus it slips in without worry in text/template
and html/template both.
Fixes long-requested issue #9969.
Change-Id: I3774be650bfa6370cb993d0899aa669c211de7b2
Reviewed-on: https://go-review.googlesource.com/14391
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/src/text/template/doc.go b/src/text/template/doc.go
index 0ce63f6..cd36f44 100644
--- a/src/text/template/doc.go
+++ b/src/text/template/doc.go
@@ -36,6 +36,31 @@
More intricate examples appear below.
+Text and spaces
+
+By default, all text between actions is copied verbatim when the template is
+executed. For example, the string " items are made of " in the example above appears
+on standard output when the program is run.
+
+However, to aid in formatting template source code, if an action's left delimiter
+(by default "{{") is followed immediately by a minus sign and ASCII space character
+("{{- "), all trailing white space is trimmed from the immediately preceding text.
+Similarly, if the right delimiter ("}}") is preceded by a space and minus sign
+(" -}}"), all leading white space is trimmed from the immediately following text.
+In these trim markers, the ASCII space must be present; "{{-3}}" parses as an
+action containing the number -3.
+
+For instance, when executing the template whose source is
+
+ "{{23 -}} < {{- 45}}"
+
+the generated output would be
+
+ "23<45"
+
+For this trimming, the definition of white space characters is the same as in Go:
+space, horizontal tab, carriage return, and newline.
+
Actions
Here is the list of actions. "Arguments" and "pipelines" are evaluations of
diff --git a/src/text/template/example_test.go b/src/text/template/example_test.go
index de1d518..cae8ff4 100644
--- a/src/text/template/example_test.go
+++ b/src/text/template/example_test.go
@@ -15,9 +15,12 @@
const letter = `
Dear {{.Name}},
{{if .Attended}}
-It was a pleasure to see you at the wedding.{{else}}
-It is a shame you couldn't make it to the wedding.{{end}}
-{{with .Gift}}Thank you for the lovely {{.}}.
+It was a pleasure to see you at the wedding.
+{{- else}}
+It is a shame you couldn't make it to the wedding.
+{{- end}}
+{{with .Gift -}}
+Thank you for the lovely {{.}}.
{{end}}
Best wishes,
Josie
diff --git a/src/text/template/exec_test.go b/src/text/template/exec_test.go
index 07ebb55..9fd0132 100644
--- a/src/text/template/exec_test.go
+++ b/src/text/template/exec_test.go
@@ -797,18 +797,19 @@
}
// Use different delimiters to test Set.Delims.
+// Also test the trimming of leading and trailing spaces.
const treeTemplate = `
- (define "tree")
+ (- define "tree" -)
[
- (.Val)
- (with .Left)
- (template "tree" .)
- (end)
- (with .Right)
- (template "tree" .)
- (end)
+ (- .Val -)
+ (- with .Left -)
+ (template "tree" . -)
+ (- end -)
+ (- with .Right -)
+ (- template "tree" . -)
+ (- end -)
]
- (end)
+ (- end -)
`
func TestTree(t *testing.T) {
@@ -853,19 +854,13 @@
t.Fatal("parse error:", err)
}
var b bytes.Buffer
- stripSpace := func(r rune) rune {
- if r == '\t' || r == '\n' {
- return -1
- }
- return r
- }
const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]"
// First by looking up the template.
err = tmpl.Lookup("tree").Execute(&b, tree)
if err != nil {
t.Fatal("exec error:", err)
}
- result := strings.Map(stripSpace, b.String())
+ result := b.String()
if result != expect {
t.Errorf("expected %q got %q", expect, result)
}
@@ -875,7 +870,7 @@
if err != nil {
t.Fatal("exec error:", err)
}
- result = strings.Map(stripSpace, b.String())
+ result = b.String()
if result != expect {
t.Errorf("expected %q got %q", expect, result)
}
diff --git a/src/text/template/parse/lex.go b/src/text/template/parse/lex.go
index 8f9fe1d..9061731 100644
--- a/src/text/template/parse/lex.go
+++ b/src/text/template/parse/lex.go
@@ -83,6 +83,21 @@
const eof = -1
+// Trimming spaces.
+// If the action begins "{{- " rather than "{{", then all space/tab/newlines
+// preceding the action are trimmed; conversely if it ends " -}}" the
+// leading spaces are trimmed. This is done entirely in the lexer; the
+// parser never sees it happen. We require an ASCII space to be
+// present to avoid ambiguity with things like "{{-3}}". It reads
+// better with the space present anyway. For simplicity, only ASCII
+// space does the job.
+const (
+ spaceChars = " \t\r\n" // These are the space characters defined by Go itself.
+ leftTrimMarker = "- " // Attached to left delimiter, trims trailing spaces from preceding text.
+ rightTrimMarker = " -" // Attached to right delimiter, trims leading spaces from following text.
+ trimMarkerLen = Pos(len(leftTrimMarker))
+)
+
// stateFn represents the state of the scanner as a function that returns the next state.
type stateFn func(*lexer) stateFn
@@ -220,10 +235,18 @@
// lexText scans until an opening action delimiter, "{{".
func lexText(l *lexer) stateFn {
for {
- if strings.HasPrefix(l.input[l.pos:], l.leftDelim) {
+ delim, trimSpace := l.atLeftDelim()
+ if delim {
+ trimLength := Pos(0)
+ if trimSpace {
+ trimLength = rightTrimLength(l.input[l.start:l.pos])
+ }
+ l.pos -= trimLength
if l.pos > l.start {
l.emit(itemText)
}
+ l.pos += trimLength
+ l.ignore()
return lexLeftDelim
}
if l.next() == eof {
@@ -238,13 +261,56 @@
return nil
}
-// lexLeftDelim scans the left delimiter, which is known to be present.
+// atLeftDelim reports whether the lexer is at a left delimiter, possibly followed by a trim marker.
+func (l *lexer) atLeftDelim() (delim, trimSpaces bool) {
+ if !strings.HasPrefix(l.input[l.pos:], l.leftDelim) {
+ return false, false
+ }
+ // The left delim might have the marker afterwards.
+ trimSpaces = strings.HasPrefix(l.input[l.pos+Pos(len(l.leftDelim)):], leftTrimMarker)
+ return true, trimSpaces
+}
+
+// rightTrimLength returns the length of the spaces at the end of the string.
+func rightTrimLength(s string) Pos {
+ return Pos(len(s) - len(strings.TrimRight(s, spaceChars)))
+}
+
+// atRightDelim reports whether the lexer is at a right delimiter, possibly preceded by a trim marker.
+func (l *lexer) atRightDelim() (delim, trimSpaces bool) {
+ if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
+ return true, false
+ }
+ // The right delim might have the marker before.
+ if strings.HasPrefix(l.input[l.pos:], rightTrimMarker) {
+ if strings.HasPrefix(l.input[l.pos+trimMarkerLen:], l.rightDelim) {
+ return true, true
+ }
+ }
+ return false, false
+}
+
+// leftTrimLength returns the length of the spaces at the beginning of the string.
+func leftTrimLength(s string) Pos {
+ return Pos(len(s) - len(strings.TrimLeft(s, spaceChars)))
+}
+
+// lexLeftDelim scans the left delimiter, which is known to be present, possibly with a trim marker.
func lexLeftDelim(l *lexer) stateFn {
l.pos += Pos(len(l.leftDelim))
- if strings.HasPrefix(l.input[l.pos:], leftComment) {
+ trimSpace := strings.HasPrefix(l.input[l.pos:], leftTrimMarker)
+ afterMarker := Pos(0)
+ if trimSpace {
+ afterMarker = trimMarkerLen
+ }
+ if strings.HasPrefix(l.input[l.pos+afterMarker:], leftComment) {
+ l.pos += afterMarker
+ l.ignore()
return lexComment
}
l.emit(itemLeftDelim)
+ l.pos += afterMarker
+ l.ignore()
l.parenDepth = 0
return lexInsideAction
}
@@ -257,19 +323,34 @@
return l.errorf("unclosed comment")
}
l.pos += Pos(i + len(rightComment))
- if !strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
+ delim, trimSpace := l.atRightDelim()
+ if !delim {
return l.errorf("comment ends before closing delimiter")
-
+ }
+ if trimSpace {
+ l.pos += trimMarkerLen
}
l.pos += Pos(len(l.rightDelim))
+ if trimSpace {
+ l.pos += leftTrimLength(l.input[l.pos:])
+ }
l.ignore()
return lexText
}
-// lexRightDelim scans the right delimiter, which is known to be present.
+// lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker.
func lexRightDelim(l *lexer) stateFn {
+ trimSpace := strings.HasPrefix(l.input[l.pos:], rightTrimMarker)
+ if trimSpace {
+ l.pos += trimMarkerLen
+ l.ignore()
+ }
l.pos += Pos(len(l.rightDelim))
l.emit(itemRightDelim)
+ if trimSpace {
+ l.pos += leftTrimLength(l.input[l.pos:])
+ l.ignore()
+ }
return lexText
}
@@ -278,7 +359,8 @@
// Either number, quoted string, or identifier.
// Spaces separate arguments; runs of spaces turn into itemSpace.
// Pipe symbols separate and are emitted.
- if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
+ delim, _ := l.atRightDelim()
+ if delim {
if l.parenDepth == 0 {
return lexRightDelim
}
diff --git a/src/text/template/parse/lex_test.go b/src/text/template/parse/lex_test.go
index be551d8..17dbe28 100644
--- a/src/text/template/parse/lex_test.go
+++ b/src/text/template/parse/lex_test.go
@@ -278,6 +278,19 @@
tRight,
tEOF,
}},
+ {"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{
+ {itemText, 0, "hello-"},
+ tLeft,
+ {itemNumber, 0, "3"},
+ tRight,
+ {itemText, 0, "-world"},
+ tEOF,
+ }},
+ {"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{
+ {itemText, 0, "hello-"},
+ {itemText, 0, "-world"},
+ tEOF,
+ }},
// errors
{"badchar", "#{{\x01}}", []item{
{itemText, 0, "#"},
@@ -339,7 +352,7 @@
{itemText, 0, "hello-"},
{itemError, 0, `unclosed comment`},
}},
- {"text with comment close separted from delim", "hello-{{/* */ }}-world", []item{
+ {"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{
{itemText, 0, "hello-"},
{itemError, 0, `comment ends before closing delimiter`},
}},
diff --git a/src/text/template/parse/parse_test.go b/src/text/template/parse/parse_test.go
index 200d50c..28b5f7c 100644
--- a/src/text/template/parse/parse_test.go
+++ b/src/text/template/parse/parse_test.go
@@ -228,6 +228,13 @@
`{{with .X}}"hello"{{end}}`},
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
+ // Trimming spaces.
+ {"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
+ {"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
+ {"trim left and right", "x \r\n\t{{- 3 -}}\n\n\ty", noError, `"x"{{3}}"y"`},
+ {"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
+ {"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
+ {"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
// Errors.
{"unclosed action", "hello{{range", hasError, ""},
{"unmatched end", "{{end}}", hasError, ""},