diff --git a/error.go b/error.go new file mode 100644 index 0000000..3cd5c5c --- /dev/null +++ b/error.go @@ -0,0 +1,66 @@ +package mustache + +import ( + "fmt" +) + +// ErrorCode is the list of allowed values for the error's code. +type ErrorCode string + +// List of values that ErrorCode can take. +const ( + ErrUnmatchedOpenTag ErrorCode = "unmatched_open_tag" + ErrEmptyTag ErrorCode = "empty_tag" + ErrSectionNoClosingTag ErrorCode = "section_no_closing_tag" + ErrInterleavedClosingTag ErrorCode = "interleaved_closing_tag" + ErrInvalidMetaTag ErrorCode = "invalid_meta_tag" + ErrUnmatchedCloseTag ErrorCode = "unmatched_close_tag" +) + +// ParseError represents an error during the parsing +type ParseError struct { + // Line contains the line of the error + Line int + // Code contains the error code of the error + Code ErrorCode + // Reason contains the name of the element generating the error + Reason string +} + +func (e ParseError) Error() string { + return fmt.Sprintf("line %d: %s", e.Line, e.defaultMessage()) +} + +func (e ParseError) defaultMessage() string { + switch e.Code { + case ErrUnmatchedOpenTag: + return "unmatched open tag" + case ErrEmptyTag: + return "empty tag" + case ErrSectionNoClosingTag: + return fmt.Sprintf("Section %s has no closing tag", e.Reason) + case ErrInterleavedClosingTag: + return fmt.Sprintf("interleaved closing tag: %s", e.Reason) + case ErrInvalidMetaTag: + return "Invalid meta tag" + case ErrUnmatchedCloseTag: + return "unmatched close tag" + default: + return "unknown error" + } +} + +func newError(line int, code ErrorCode) ParseError { + return ParseError{ + Line: line, + Code: code, + } +} + +func newErrorWithReason(line int, code ErrorCode, reason string) ParseError { + return ParseError{ + Line: line, + Code: code, + Reason: reason, + } +} diff --git a/mustache.go b/mustache.go index 5756b68..a6cdff7 100644 --- a/mustache.go +++ b/mustache.go @@ -110,11 +110,6 @@ type Template struct { partial PartialProvider } -type parseError struct { - line int - message string -} - // Tags returns the mustache tags for the given template func (tmpl *Template) Tags() []Tag { return extractTags(tmpl.elems) @@ -174,10 +169,6 @@ func (e *partialElement) Tags() []Tag { return nil } -func (p parseError) Error() string { - return fmt.Sprintf("line %d: %s", p.line, p.message) -} - func (tmpl *Template) readString(s string) (string, error) { newlines := 0 for i := tmpl.p; ; i++ { @@ -270,7 +261,7 @@ func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) { if err == io.EOF { //put the remaining text in a block - return nil, parseError{tmpl.curline, "unmatched open tag"} + return nil, newError(tmpl.curline, ErrUnmatchedOpenTag) } text = text[:len(text)-len(tmpl.ctag)] @@ -278,7 +269,7 @@ func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) { //trim the close tag off the text tag := strings.TrimSpace(text) if len(tag) == 0 { - return nil, parseError{tmpl.curline, "empty tag"} + return nil, newError(tmpl.curline, ErrEmptyTag) } eow := tmpl.p @@ -334,7 +325,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error { if err == io.EOF { //put the remaining text in a block - return parseError{section.startline, "Section " + section.name + " has no closing tag"} + return newErrorWithReason(section.startline, ErrSectionNoClosingTag, section.name) } // put text into an item @@ -364,7 +355,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error { case '/': name := strings.TrimSpace(tag[1:]) if name != section.name { - return parseError{tmpl.curline, "interleaved closing tag: " + name} + return newErrorWithReason(tmpl.curline, ErrInterleavedClosingTag, name) } return nil case '>': @@ -376,7 +367,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error { section.elems = append(section.elems, partial) case '=': if tag[len(tag)-1] != '=' { - return parseError{tmpl.curline, "Invalid meta tag"} + return newError(tmpl.curline, ErrInvalidMetaTag) } tag = strings.TrimSpace(tag[1 : len(tag)-1]) newtags := strings.SplitN(tag, " ", 2) @@ -437,7 +428,7 @@ func (tmpl *Template) parse() error { } tmpl.elems = append(tmpl.elems, &se) case '/': - return parseError{tmpl.curline, "unmatched close tag"} + return newError(tmpl.curline, ErrUnmatchedCloseTag) case '>': name := strings.TrimSpace(tag[1:]) partial, err := tmpl.parsePartial(name, textResult.padding) @@ -447,7 +438,7 @@ func (tmpl *Template) parse() error { tmpl.elems = append(tmpl.elems, partial) case '=': if tag[len(tag)-1] != '=' { - return parseError{tmpl.curline, "Invalid meta tag"} + return newError(tmpl.curline, ErrInvalidMetaTag) } tag = strings.TrimSpace(tag[1 : len(tag)-1]) newtags := strings.SplitN(tag, " ", 2) diff --git a/mustache_test.go b/mustache_test.go index 847c45f..938291e 100644 --- a/mustache_test.go +++ b/mustache_test.go @@ -2,6 +2,7 @@ package mustache import ( "bytes" + "errors" "fmt" "os" "path" @@ -459,6 +460,40 @@ func TestMalformed(t *testing.T) { } } +type TestWithParseError struct { + *Test + errLine int + errCode ErrorCode + errReason string +} + +var malformedWithParseError = []TestWithParseError{ + {Test: &Test{`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")}, errLine: 1, errCode: ErrEmptyTag, errReason: ""}, + {Test: &Test{`{{}}`, nil, "", fmt.Errorf("line 1: empty tag")}, errLine: 1, errCode: ErrEmptyTag, errReason: ""}, + {Test: &Test{`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, errLine: 1, errCode: ErrUnmatchedOpenTag, errReason: ""}, + {Test: &Test{`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, errLine: 1, errCode: ErrUnmatchedOpenTag, errReason: ""}, + // invalid syntax - https://github.com/hoisie/mustache/issues/10 + {Test: &Test{`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")}, errLine: 1, errCode: ErrInterleavedClosingTag, errReason: "a"}, +} + +func TestParseError(t *testing.T) { + for _, test := range malformedWithParseError { + output, err := Render(test.tmpl, test.context) + if err != nil { + var parseError ParseError + if errors.As(err, &parseError) { + if parseError.Line != test.errLine || parseError.Code != test.errCode || parseError.Reason != test.errReason { + t.Errorf("%q expected ParseError (line %q code %q reason %q) but got (line %q code %q reason %q)", test.tmpl, test.errLine, test.errCode, test.errReason, parseError.Line, parseError.Code, parseError.Reason) + } + } else { + t.Errorf("%q expected ParseError (line %q code %q reason %q) but got %q", test.tmpl, test.errLine, test.errCode, test.errReason, test.err.Error()) + } + } else { + t.Errorf("%q expected error %q but got %q", test.tmpl, test.err.Error(), output) + } + } +} + type LayoutTest struct { layout string tmpl string