diff options
| author | Rob Pike <r@golang.org> | 2015-09-08 14:58:12 -0700 |
|---|---|---|
| committer | Rob Pike <r@golang.org> | 2015-09-09 05:28:11 +0000 |
| commit | e6ee26a03b79d0e8b658463bdb29349ca68e1460 (patch) | |
| tree | 3c8ba47d66ba2ba5d9d00675a1fb67de8b4ebd08 /src/text/template/parse | |
| parent | 49fb8cc10c2d61ebdf3829f42bba9bec7b0a7ff7 (diff) | |
| download | go-e6ee26a03b79d0e8b658463bdb29349ca68e1460.tar.xz | |
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>
Diffstat (limited to 'src/text/template/parse')
| -rw-r--r-- | src/text/template/parse/lex.go | 96 | ||||
| -rw-r--r-- | src/text/template/parse/lex_test.go | 15 | ||||
| -rw-r--r-- | src/text/template/parse/parse_test.go | 7 |
3 files changed, 110 insertions, 8 deletions
diff --git a/src/text/template/parse/lex.go b/src/text/template/parse/lex.go index 8f9fe1d4d8..9061731b2b 100644 --- a/src/text/template/parse/lex.go +++ b/src/text/template/parse/lex.go @@ -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 @@ -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 { @@ -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 } @@ -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 } @@ -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 } diff --git a/src/text/template/parse/lex_test.go b/src/text/template/parse/lex_test.go index be551d8780..17dbe28a9f 100644 --- a/src/text/template/parse/lex_test.go +++ b/src/text/template/parse/lex_test.go @@ -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, "#"}, @@ -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`}, }}, diff --git a/src/text/template/parse/parse_test.go b/src/text/template/parse/parse_test.go index 200d50c194..28b5f7cb90 100644 --- a/src/text/template/parse/parse_test.go +++ b/src/text/template/parse/parse_test.go @@ -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, ""}, |
