Commit e6ee26a0 authored by Rob Pike's avatar Rob Pike

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/14391Reviewed-by: default avatarAndrew Gerrand <adg@golang.org>
parent 49fb8cc1
...@@ -36,6 +36,31 @@ Here is a trivial example that prints "17 items are made of wool". ...@@ -36,6 +36,31 @@ Here is a trivial example that prints "17 items are made of wool".
More intricate examples appear below. 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 Actions
Here is the list of actions. "Arguments" and "pipelines" are evaluations of Here is the list of actions. "Arguments" and "pipelines" are evaluations of
......
...@@ -15,9 +15,12 @@ func ExampleTemplate() { ...@@ -15,9 +15,12 @@ func ExampleTemplate() {
const letter = ` const letter = `
Dear {{.Name}}, Dear {{.Name}},
{{if .Attended}} {{if .Attended}}
It was a pleasure to see you at the wedding.{{else}} It was a pleasure to see you at the wedding.
It is a shame you couldn't make it to the wedding.{{end}} {{- else}}
{{with .Gift}}Thank you for the lovely {{.}}. It is a shame you couldn't make it to the wedding.
{{- end}}
{{with .Gift -}}
Thank you for the lovely {{.}}.
{{end}} {{end}}
Best wishes, Best wishes,
Josie Josie
......
...@@ -797,18 +797,19 @@ type Tree struct { ...@@ -797,18 +797,19 @@ type Tree struct {
} }
// Use different delimiters to test Set.Delims. // Use different delimiters to test Set.Delims.
// Also test the trimming of leading and trailing spaces.
const treeTemplate = ` const treeTemplate = `
(define "tree") (- define "tree" -)
[ [
(.Val) (- .Val -)
(with .Left) (- with .Left -)
(template "tree" .) (template "tree" . -)
(end) (- end -)
(with .Right) (- with .Right -)
(template "tree" .) (- template "tree" . -)
(end) (- end -)
] ]
(end) (- end -)
` `
func TestTree(t *testing.T) { func TestTree(t *testing.T) {
...@@ -853,19 +854,13 @@ func TestTree(t *testing.T) { ...@@ -853,19 +854,13 @@ func TestTree(t *testing.T) {
t.Fatal("parse error:", err) t.Fatal("parse error:", err)
} }
var b bytes.Buffer 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]]]]" const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]"
// First by looking up the template. // First by looking up the template.
err = tmpl.Lookup("tree").Execute(&b, tree) err = tmpl.Lookup("tree").Execute(&b, tree)
if err != nil { if err != nil {
t.Fatal("exec error:", err) t.Fatal("exec error:", err)
} }
result := strings.Map(stripSpace, b.String()) result := b.String()
if result != expect { if result != expect {
t.Errorf("expected %q got %q", expect, result) t.Errorf("expected %q got %q", expect, result)
} }
...@@ -875,7 +870,7 @@ func TestTree(t *testing.T) { ...@@ -875,7 +870,7 @@ func TestTree(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("exec error:", err) t.Fatal("exec error:", err)
} }
result = strings.Map(stripSpace, b.String()) result = b.String()
if result != expect { if result != expect {
t.Errorf("expected %q got %q", expect, result) t.Errorf("expected %q got %q", expect, result)
} }
......
...@@ -83,6 +83,21 @@ var key = map[string]itemType{ ...@@ -83,6 +83,21 @@ var key = map[string]itemType{
const eof = -1 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. // stateFn represents the state of the scanner as a function that returns the next state.
type stateFn func(*lexer) stateFn type stateFn func(*lexer) stateFn
...@@ -220,10 +235,18 @@ const ( ...@@ -220,10 +235,18 @@ const (
// lexText scans until an opening action delimiter, "{{". // lexText scans until an opening action delimiter, "{{".
func lexText(l *lexer) stateFn { func lexText(l *lexer) stateFn {
for { 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 { if l.pos > l.start {
l.emit(itemText) l.emit(itemText)
} }
l.pos += trimLength
l.ignore()
return lexLeftDelim return lexLeftDelim
} }
if l.next() == eof { if l.next() == eof {
...@@ -238,13 +261,56 @@ func lexText(l *lexer) stateFn { ...@@ -238,13 +261,56 @@ func lexText(l *lexer) stateFn {
return nil 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 { func lexLeftDelim(l *lexer) stateFn {
l.pos += Pos(len(l.leftDelim)) 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 return lexComment
} }
l.emit(itemLeftDelim) l.emit(itemLeftDelim)
l.pos += afterMarker
l.ignore()
l.parenDepth = 0 l.parenDepth = 0
return lexInsideAction return lexInsideAction
} }
...@@ -257,19 +323,34 @@ func lexComment(l *lexer) stateFn { ...@@ -257,19 +323,34 @@ func lexComment(l *lexer) stateFn {
return l.errorf("unclosed comment") return l.errorf("unclosed comment")
} }
l.pos += Pos(i + len(rightComment)) 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") return l.errorf("comment ends before closing delimiter")
}
if trimSpace {
l.pos += trimMarkerLen
} }
l.pos += Pos(len(l.rightDelim)) l.pos += Pos(len(l.rightDelim))
if trimSpace {
l.pos += leftTrimLength(l.input[l.pos:])
}
l.ignore() l.ignore()
return lexText 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 { 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.pos += Pos(len(l.rightDelim))
l.emit(itemRightDelim) l.emit(itemRightDelim)
if trimSpace {
l.pos += leftTrimLength(l.input[l.pos:])
l.ignore()
}
return lexText return lexText
} }
...@@ -278,7 +359,8 @@ func lexInsideAction(l *lexer) stateFn { ...@@ -278,7 +359,8 @@ func lexInsideAction(l *lexer) stateFn {
// Either number, quoted string, or identifier. // Either number, quoted string, or identifier.
// Spaces separate arguments; runs of spaces turn into itemSpace. // Spaces separate arguments; runs of spaces turn into itemSpace.
// Pipe symbols separate and are emitted. // Pipe symbols separate and are emitted.
if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { delim, _ := l.atRightDelim()
if delim {
if l.parenDepth == 0 { if l.parenDepth == 0 {
return lexRightDelim return lexRightDelim
} }
......
...@@ -278,6 +278,19 @@ var lexTests = []lexTest{ ...@@ -278,6 +278,19 @@ var lexTests = []lexTest{
tRight, tRight,
tEOF, 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 // errors
{"badchar", "#{{\x01}}", []item{ {"badchar", "#{{\x01}}", []item{
{itemText, 0, "#"}, {itemText, 0, "#"},
...@@ -339,7 +352,7 @@ var lexTests = []lexTest{ ...@@ -339,7 +352,7 @@ var lexTests = []lexTest{
{itemText, 0, "hello-"}, {itemText, 0, "hello-"},
{itemError, 0, `unclosed comment`}, {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-"}, {itemText, 0, "hello-"},
{itemError, 0, `comment ends before closing delimiter`}, {itemError, 0, `comment ends before closing delimiter`},
}}, }},
......
...@@ -228,6 +228,13 @@ var parseTests = []parseTest{ ...@@ -228,6 +228,13 @@ var parseTests = []parseTest{
`{{with .X}}"hello"{{end}}`}, `{{with .X}}"hello"{{end}}`},
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError, {"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`}, `{{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. // Errors.
{"unclosed action", "hello{{range", hasError, ""}, {"unclosed action", "hello{{range", hasError, ""},
{"unmatched end", "{{end}}", hasError, ""}, {"unmatched end", "{{end}}", hasError, ""},
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment