Skip to content
This repository has been archived by the owner on Jun 7, 2019. It is now read-only.

Commit

Permalink
text/template: provide a way to trim leading and trailing space betwe…
Browse files Browse the repository at this point in the history
…en 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 golang#9969.

Change-Id: I3774be650bfa6370cb993d0899aa669c211de7b2
Reviewed-on: https://go-review.googlesource.com/14391
Reviewed-by: Andrew Gerrand <[email protected]>
  • Loading branch information
robpike committed Sep 9, 2015
1 parent 49fb8cc commit e6ee26a
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 28 deletions.
25 changes: 25 additions & 0 deletions src/text/template/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,31 @@ Here is a trivial example that prints "17 items are made of wool".
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
Expand Down
9 changes: 6 additions & 3 deletions src/text/template/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ func ExampleTemplate() {
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
Expand Down
29 changes: 12 additions & 17 deletions src/text/template/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,18 +797,19 @@ type Tree struct {
}

// 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) {
Expand Down Expand Up @@ -853,19 +854,13 @@ func TestTree(t *testing.T) {
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)
}
Expand All @@ -875,7 +870,7 @@ func TestTree(t *testing.T) {
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)
}
Expand Down
96 changes: 89 additions & 7 deletions src/text/template/parse/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ var key = map[string]itemType{

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

Expand Down Expand Up @@ -220,10 +235,18 @@ const (
// 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 {
Expand All @@ -238,13 +261,56 @@ func lexText(l *lexer) stateFn {
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
}
Expand All @@ -257,19 +323,34 @@ func lexComment(l *lexer) stateFn {
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
}

Expand All @@ -278,7 +359,8 @@ func lexInsideAction(l *lexer) stateFn {
// 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
}
Expand Down
15 changes: 14 additions & 1 deletion src/text/template/parse/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,19 @@ var lexTests = []lexTest{
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, "#"},
Expand Down Expand Up @@ -339,7 +352,7 @@ var lexTests = []lexTest{
{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`},
}},
Expand Down
7 changes: 7 additions & 0 deletions src/text/template/parse/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ var parseTests = []parseTest{
`{{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, ""},
Expand Down

0 comments on commit e6ee26a

Please sign in to comment.