t.Fatal("expected error")
}
str := err.Error()
- if !strings.Contains(str, "X:3: unexpected unterminated raw quoted string") {
+ if !strings.Contains(str, "X:3: unterminated raw quoted string") {
t.Fatalf("unexpected error: %s", str)
}
}
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.
+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 white
+space, all trailing white space is trimmed from the immediately preceding text.
+Similarly, if the right delimiter ("}}") is preceded by white space and a minus
+sign, all leading white space is trimmed from the immediately following text.
+In these trim markers, the white space must be present:
+"{{- 3}}" is like "{{3}}" but trims the immediately preceding text, while
+"{{-3}}" parses as an action containing the number -3.
For instance, when executing the template whose source is
t.Fatal("expected error")
}
str := err.Error()
- if !strings.Contains(str, "X:3: unexpected unterminated raw quoted string") {
+ if !strings.Contains(str, "X:3: unterminated raw quoted string") {
t.Fatalf("unexpected error: %s", str)
}
}
// 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
+// parser never sees it happen. We require an ASCII space (' ', \t, \r, \n)
+// 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.
+// 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))
+ spaceChars = " \t\r\n" // These are the space characters defined by Go itself.
+ trimMarker = '-' // Attached to left/right delimiter, trims trailing spaces from preceding/following text.
+ trimMarkerLen = Pos(1 + 1) // marker plus space before or after
)
// stateFn represents the state of the scanner as a function that returns the next state.
// lexer holds the state of the scanner.
type lexer struct {
- name string // the name of the input; used only for error reports
- input string // the string being scanned
- leftDelim string // start of action
- rightDelim string // end of action
- trimRightDelim string // end of action with trim marker
- emitComment bool // emit itemComment tokens.
- pos Pos // current position in the input
- start Pos // start position of this item
- width Pos // width of last rune read from input
- items chan item // channel of scanned items
- parenDepth int // nesting depth of ( ) exprs
- line int // 1+number of newlines seen
- startLine int // start line of this item
+ name string // the name of the input; used only for error reports
+ input string // the string being scanned
+ leftDelim string // start of action
+ rightDelim string // end of action
+ emitComment bool // emit itemComment tokens.
+ pos Pos // current position in the input
+ start Pos // start position of this item
+ width Pos // width of last rune read from input
+ items chan item // channel of scanned items
+ parenDepth int // nesting depth of ( ) exprs
+ line int // 1+number of newlines seen
+ startLine int // start line of this item
}
// next returns the next rune in the input.
right = rightDelim
}
l := &lexer{
- name: name,
- input: input,
- leftDelim: left,
- rightDelim: right,
- trimRightDelim: rightTrimMarker + right,
- emitComment: emitComment,
- items: make(chan item),
- line: 1,
- startLine: 1,
+ name: name,
+ input: input,
+ leftDelim: left,
+ rightDelim: right,
+ emitComment: emitComment,
+ items: make(chan item),
+ line: 1,
+ startLine: 1,
}
go l.run()
return l
ldn := Pos(len(l.leftDelim))
l.pos += Pos(x)
trimLength := Pos(0)
- if strings.HasPrefix(l.input[l.pos+ldn:], leftTrimMarker) {
+ if hasLeftTrimMarker(l.input[l.pos+ldn:]) {
trimLength = rightTrimLength(l.input[l.start:l.pos])
}
l.pos -= trimLength
// 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.trimRightDelim) { // With trim marker.
+ if hasRightTrimMarker(l.input[l.pos:]) && strings.HasPrefix(l.input[l.pos+trimMarkerLen:], l.rightDelim) { // With trim marker.
return true, true
}
if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { // Without trim marker.
// 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))
- trimSpace := strings.HasPrefix(l.input[l.pos:], leftTrimMarker)
+ trimSpace := hasLeftTrimMarker(l.input[l.pos:])
afterMarker := Pos(0)
if trimSpace {
afterMarker = trimMarkerLen
// 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)
+ trimSpace := hasRightTrimMarker(l.input[l.pos:])
if trimSpace {
l.pos += trimMarkerLen
l.ignore()
return l.errorf("unclosed left paren")
}
switch r := l.next(); {
- case r == eof || isEndOfLine(r):
+ case r == eof:
return l.errorf("unclosed action")
case isSpace(r):
l.backup() // Put space back in case we have " -}}".
}
// Be careful about a trim-marked closing delimiter, which has a minus
// after a space. We know there is a space, so check for the '-' that might follow.
- if strings.HasPrefix(l.input[l.pos-1:], l.trimRightDelim) {
+ if hasRightTrimMarker(l.input[l.pos-1:]) && strings.HasPrefix(l.input[l.pos-1+trimMarkerLen:], l.rightDelim) {
l.backup() // Before the space.
if numSpaces == 1 {
return lexRightDelim // On the delim, so go right to that.
// day to implement arithmetic.
func (l *lexer) atTerminator() bool {
r := l.peek()
- if isSpace(r) || isEndOfLine(r) {
+ if isSpace(r) {
return true
}
switch r {
// isSpace reports whether r is a space character.
func isSpace(r rune) bool {
- return r == ' ' || r == '\t'
-}
-
-// isEndOfLine reports whether r is an end-of-line character.
-func isEndOfLine(r rune) bool {
- return r == '\r' || r == '\n'
+ return r == ' ' || r == '\t' || r == '\r' || r == '\n'
}
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
func isAlphaNumeric(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}
+
+func hasLeftTrimMarker(s string) bool {
+ return len(s) >= 2 && s[0] == trimMarker && isSpace(rune(s[1]))
+}
+
+func hasRightTrimMarker(s string) bool {
+ return len(s) >= 2 && isSpace(rune(s[0])) && s[1] == trimMarker
+}
tLeft,
mkItem(itemError, "unrecognized character in action: U+0001"),
}},
- {"unclosed action", "{{\n}}", []item{
+ {"unclosed action", "{{", []item{
tLeft,
mkItem(itemError, "unclosed action"),
}},
Mode Mode // parsing mode.
text string // text parsed to create the template (or its parent)
// Parsing only; cleared after parse.
- funcs []map[string]interface{}
- lex *lexer
- token [3]item // three-token lookahead for parser.
- peekCount int
- vars []string // variables defined at the moment.
- treeSet map[string]*Tree
- mode Mode
+ funcs []map[string]interface{}
+ lex *lexer
+ token [3]item // three-token lookahead for parser.
+ peekCount int
+ vars []string // variables defined at the moment.
+ treeSet map[string]*Tree
+ actionLine int // line of left delim starting action
+ mode Mode
}
// A mode value is a set of flags (or 0). Modes control parser behavior.
// unexpected complains about the token and terminates processing.
func (t *Tree) unexpected(token item, context string) {
+ if token.typ == itemError {
+ extra := ""
+ if t.actionLine != 0 && t.actionLine != token.line {
+ extra = fmt.Sprintf(" in action started at %s:%d", t.ParseName, t.actionLine)
+ if strings.HasSuffix(token.val, " action") {
+ extra = extra[len(" in action"):] // avoid "action in action"
+ }
+ }
+ t.errorf("%s%s", token, extra)
+ }
t.errorf("unexpected %s in %s", token, context)
}
case itemText:
return t.newText(token.pos, token.val)
case itemLeftDelim:
+ t.actionLine = token.line
+ defer t.clearActionLine()
return t.action()
case itemComment:
return t.newComment(token.pos, token.val)
return nil
}
+func (t *Tree) clearActionLine() {
+ t.actionLine = 0
+}
+
// Action:
// control
// command ("|" command)*
t.backup()
token := t.peek()
// Do not pop variables; they persist until "end".
- return t.newAction(token.pos, token.line, t.pipeline("command"))
+ return t.newAction(token.pos, token.line, t.pipeline("command", itemRightDelim))
}
// Pipeline:
// declarations? command ('|' command)*
-func (t *Tree) pipeline(context string) (pipe *PipeNode) {
+func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) {
token := t.peekNonSpace()
pipe = t.newPipeline(token.pos, token.line, nil)
// Are there declarations or assignments?
}
for {
switch token := t.nextNonSpace(); token.typ {
- case itemRightDelim, itemRightParen:
+ case end:
// At this point, the pipeline is complete
t.checkPipeline(pipe, context)
- if token.typ == itemRightParen {
- t.backup()
- }
return
case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier,
itemNumber, itemNil, itemRawString, itemString, itemVariable, itemLeftParen:
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
defer t.popVars(len(t.vars))
- pipe = t.pipeline(context)
+ pipe = t.pipeline(context, itemRightDelim)
var next Node
list, next = t.itemList()
switch next.Type() {
token := t.nextNonSpace()
name := t.parseTemplateName(token, context)
- pipe := t.pipeline(context)
+ pipe := t.pipeline(context, itemRightDelim)
block := New(name) // name will be updated once we know it.
block.text = t.text
if t.nextNonSpace().typ != itemRightDelim {
t.backup()
// Do not pop variables; they persist until "end".
- pipe = t.pipeline(context)
+ pipe = t.pipeline(context, itemRightDelim)
}
return t.newTemplate(token.pos, token.line, name, pipe)
}
switch token := t.next(); token.typ {
case itemSpace:
continue
- case itemError:
- t.errorf("%s", token.val)
case itemRightDelim, itemRightParen:
t.backup()
case itemPipe:
+ // nothing here; break loop below
default:
- t.errorf("unexpected %s in operand", token)
+ t.unexpected(token, "operand")
}
break
}
// A nil return means the next item is not a term.
func (t *Tree) term() Node {
switch token := t.nextNonSpace(); token.typ {
- case itemError:
- t.errorf("%s", token.val)
case itemIdentifier:
if !t.hasFunction(token.val) {
t.errorf("function %q not defined", token.val)
}
return number
case itemLeftParen:
- pipe := t.pipeline("parenthesized pipeline")
- if token := t.next(); token.typ != itemRightParen {
- t.errorf("unclosed right paren: unexpected %s", token)
- }
- return pipe
+ return t.pipeline("parenthesized pipeline", itemRightParen)
case itemString, itemRawString:
s, err := strconv.Unquote(token.val)
if err != nil {
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
`{{template "foo" .}}`},
+
+ {"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
+ {"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
+ {"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
+ {"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
+ {"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
+
// Errors.
{"unclosed action", "hello{{range", hasError, ""},
{"unmatched end", "{{end}}", hasError, ""},
// Check line numbers are accurate.
{"unclosed1",
"line1\n{{",
- hasError, `unclosed1:2: unexpected unclosed action in command`},
+ hasError, `unclosed1:2: unclosed action`},
{"unclosed2",
"line1\n{{define `x`}}line2\n{{",
- hasError, `unclosed2:3: unexpected unclosed action in command`},
+ hasError, `unclosed2:3: unclosed action`},
+ {"unclosed3",
+ "line1\n{{\"x\"\n\"y\"\n",
+ hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
+ {"unclosed4",
+ "{{\n\n\n\n\n",
+ hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
+ {"var1",
+ "line1\n{{\nx\n}}",
+ hasError, `var1:3: function "x" not defined`},
// Specific errors.
{"function",
"{{foo}}",
hasError, `function "foo" not defined`},
- {"comment",
+ {"comment1",
"{{/*}}",
- hasError, `unclosed comment`},
+ hasError, `comment1:1: unclosed comment`},
+ {"comment2",
+ "{{/*\nhello\n}}",
+ hasError, `comment2:1: unclosed comment`},
{"lparen",
"{{.X (1 2 3}}",
hasError, `unclosed left paren`},
{"rparen",
- "{{.X 1 2 3)}}",
- hasError, `unexpected ")"`},
+ "{{.X 1 2 3 ) }}",
+ hasError, `unexpected ")" in command`},
+ {"rparen2",
+ "{{(.X 1 2 3",
+ hasError, `unclosed action`},
{"space",
"{{`x`3}}",
hasError, `in operand`},
hasError, `missing value for parenthesized pipeline`},
{"multilinerawstring",
"{{ $v := `\n` }} {{",
- hasError, `multilinerawstring:2: unexpected unclosed action`},
+ hasError, `multilinerawstring:2: unclosed action`},
{"rangeundefvar",
"{{range $k}}{{end}}",
hasError, `undefined variable`},