Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions mustache.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type EscapeFunc func(text string) string
// represents. The zero TagType is not a valid type.
type TagType uint

type CustomLookup interface {
Lookup(name string) (reflect.Value, error)
}

// Defines representing the possible Tag types
const (
Invalid TagType = iota
Expand Down Expand Up @@ -180,7 +184,7 @@ func (e *partialElement) Tags() []Tag {
func (tmpl *Template) readString(s string) (string, error) {
newlines := 0
for i := tmpl.p; ; i++ {
//are we at the end of the string?
// are we at the end of the string?
if i+len(s) > len(tmpl.data) {
return tmpl.data[tmpl.p:], io.EOF
}
Expand Down Expand Up @@ -268,13 +272,13 @@ func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) {
}

if err == io.EOF {
//put the remaining text in a block
// put the remaining text in a block
return nil, newError(tmpl.curline, ErrUnmatchedOpenTag)
}

text = text[:len(text)-len(tmpl.ctag)]

//trim the close tag off the text
// trim the close tag off the text
tag := strings.TrimSpace(text)
if len(tag) == 0 {
return nil, newError(tmpl.curline, ErrEmptyTag)
Expand Down Expand Up @@ -332,7 +336,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error {
mayStandalone := textResult.mayStandalone

if err == io.EOF {
//put the remaining text in a block
// put the remaining text in a block
return newErrorWithReason(section.startline, ErrSectionNoClosingTag, section.name)
}

Expand All @@ -351,7 +355,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error {
tag := tagResult.tag
switch tag[0] {
case '!':
//ignore comment
// ignore comment
case '#', '^':
name := strings.TrimSpace(tag[1:])
se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}}
Expand Down Expand Up @@ -385,7 +389,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error {
}
case '{':
if tag[len(tag)-1] == '}' {
//use a raw tag
// use a raw tag
name := strings.TrimSpace(tag[1 : len(tag)-1])
section.elems = append(section.elems, &varElement{name, true})
}
Expand All @@ -406,7 +410,7 @@ func (tmpl *Template) parse() error {
mayStandalone := textResult.mayStandalone

if err == io.EOF {
//put the remaining text in a block
// put the remaining text in a block
tmpl.elems = append(tmpl.elems, &textElement{[]byte(text)})
return nil
}
Expand All @@ -426,7 +430,7 @@ func (tmpl *Template) parse() error {
tag := tagResult.tag
switch tag[0] {
case '!':
//ignore comment
// ignore comment
case '#', '^':
name := strings.TrimSpace(tag[1:])
se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}}
Expand Down Expand Up @@ -455,7 +459,7 @@ func (tmpl *Template) parse() error {
tmpl.ctag = newtags[1]
}
case '{':
//use a raw tag
// use a raw tag
if tag[len(tag)-1] == '}' {
name := strings.TrimSpace(tag[1 : len(tag)-1])
tmpl.elems = append(tmpl.elems, &varElement{name, true})
Expand Down Expand Up @@ -492,6 +496,15 @@ func lookup(contextChain []interface{}, name string, allowMissing bool) (reflect
Outer:
for _, ctx := range contextChain {
v := ctx.(reflect.Value)
if custom, ok := v.Interface().(CustomLookup); ok {
v, err := custom.Lookup(name)
if err != nil {
return reflect.Value{}, err
}
if v.IsValid() {
return v, nil
}
}
for v.IsValid() {
typ := v.Type()
if n := v.Type().NumMethod(); n > 0 {
Expand Down Expand Up @@ -630,7 +643,7 @@ func (tmpl *Template) renderSection(section *sectionElement, contextChain []inte

chain2 := make([]interface{}, len(contextChain)+1)
copy(chain2[1:], contextChain)
//by default we execute the section
// by default we execute the section
for _, ctx := range contexts {
chain2[0] = ctx
for _, elem := range section.elems {
Expand Down
61 changes: 50 additions & 11 deletions mustache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path"
"reflect"
"strings"
"testing"
)
Expand Down Expand Up @@ -95,7 +96,7 @@ var tests = []Test{
{`{{ a }}{{=<% %>=}}<%b %><%={{ }}=%>{{ c }}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil},
{`{{ a }}{{= <% %> =}}<%b %><%= {{ }}=%>{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil},

//section tests
// section tests
{`{{#A}}{{B}}{{/A}}`, Data{true, "hello"}, "hello", nil},
{`{{#A}}{{{B}}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2", nil},
{`{{#A}}{{B}}{{/A}}`, Data{true, "5 > 2"}, "5 &gt; 2", nil},
Expand Down Expand Up @@ -127,7 +128,7 @@ var tests = []Test{
{"{{#users}}\n{{Name}}\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\nMike\n", nil},
{"{{#users}}\r\n{{Name}}\r\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\r\nMike\r\n", nil},

//falsy: golang zero values
// falsy: golang zero values
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": nil}, "", nil},
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": false}, "", nil},
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0}, "", nil},
Expand All @@ -136,31 +137,31 @@ var tests = []Test{
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": Data{}}, "", nil},
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{}}, "", nil},
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [0]interface{}{}}, "", nil},
//falsy: special cases we disagree with golang
// falsy: special cases we disagree with golang
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": "\t"}, "", nil},
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{0}}, "Hi 0", nil},
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [1]interface{}{0}}, "Hi 0", nil},

// non-false section have their value at the top of the context
{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": "Rob"}, "Hi Rob", nil},

//section does not exist
// section does not exist
{`{{#has}}{{/has}}`, &User{"Mike", 1}, "", nil},

// implicit iterator tests
{`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []string{"a", "b", "c", "d", "e"}}, "\"(a)(b)(c)(d)(e)\"", nil},
{`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []int{1, 2, 3, 4, 5}}, "\"(1)(2)(3)(4)(5)\"", nil},
{`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []float64{1.10, 2.20, 3.30, 4.40, 5.50}}, "\"(1.1)(2.2)(3.3)(4.4)(5.5)\"", nil},

//inverted section tests
// inverted section tests
{`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]interface{}{"a": "a", "b": false, "c": "c"}, "abc", nil},
{`{{^a}}b{{/a}}`, map[string]interface{}{"a": false}, "b", nil},
{`{{^a}}b{{/a}}`, map[string]interface{}{"a": true}, "", nil},
{`{{^a}}b{{/a}}`, map[string]interface{}{"a": "nonempty string"}, "", nil},
{`{{^a}}b{{/a}}`, map[string]interface{}{"a": []string{}}, "b", nil},
{`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]string{"a": "a", "c": "c"}, "abc", nil},

//function tests
// function tests
{`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil},
{`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil},
{`{{#users}}{{Func2}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil},
Expand All @@ -174,7 +175,7 @@ var tests = []Test{
{`{{#user}}{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil},
{`{{#user}}{{#Func6}}{{#Allow}}abcd{{/Allow}}{{/Func6}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil},

//context chaining
// context chaining
{`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"section": map[string]string{"name": "world"}}, "hello world", nil},
{`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"name": "bob", "section": map[string]string{"name": "world"}}, "hello world", nil},
{`hello {{#bool}}{{#section}}{{name}}{{/section}}{{/bool}}`, map[string]interface{}{"bool": true, "section": map[string]string{"name": "world"}}, "hello world", nil},
Expand All @@ -201,7 +202,7 @@ var tests = []Test{
},
}, "working", nil},

//dotted names(dot notation)
// dotted names(dot notation)
{`"{{person.name}}" == "{{#person}}{{name}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil},
{`"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil},
{`"{{a.b.c.d.e.name}}" == "Phil"`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Phil"}}}}}}, `"Phil" == "Phil"`, nil},
Expand Down Expand Up @@ -233,11 +234,11 @@ func TestBasic(t *testing.T) {
}

var missing = []Test{
//does not exist
// does not exist
{`{{dne}}`, map[string]string{"name": "world"}, "", nil},
{`{{dne}}`, User{"Mike", 1}, "", nil},
{`{{dne}}`, &User{"Mike", 1}, "", nil},
//dotted names(dot notation)
// dotted names(dot notation)
{`"{{a.b.c}}" == ""`, map[string]interface{}{}, `"" == ""`, nil},
{`"{{a.b.c.name}}" == ""`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "c": map[string]string{"name": "Jim"}}, `"" == ""`, nil},
{`{{#a}}{{b.c}}{{/a}}`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "b": map[string]string{"c": "ERROR"}}, "", nil},
Expand Down Expand Up @@ -459,7 +460,7 @@ var malformed = []Test{
{`{{}}`, nil, "", fmt.Errorf("line 1: empty tag")},
{`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")},
{`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")},
//invalid syntax - https://github.com/hoisie/mustache/issues/10
// invalid syntax - https://github.com/hoisie/mustache/issues/10
{`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")},
}

Expand Down Expand Up @@ -667,6 +668,44 @@ var tagTests = []tagsTest{
},
}

type FakeLookup struct {
vars []string
}

func (f *FakeLookup) Lookup(name string) (reflect.Value, error) {
f.vars = append(f.vars, name)
return reflect.ValueOf(name), nil
}

func (f *FakeLookup) List() []string {
return f.vars
}

func TestLookup(t *testing.T) {
mt, err := ParseString(`
{{var1}}
{{var2}}
`)
if err != nil {
t.Error(err)
}
sl := FakeLookup{}
_, err = mt.Render(&sl)
if err != nil {
t.Error(err)
}
varList := sl.List()
if len(varList) != 2 {
t.Error("expected 2 variables got", len(varList))
}
if varList[0] != "var1" {
t.Error("expected var1 got", varList[0])
}
if varList[1] != "var2" {
t.Error("expected var2 got", varList[1])
}
}

func TestTags(t *testing.T) {
for _, test := range tagTests {
testTags(t, &test)
Expand Down