diff --git a/README.md b/README.md index a3d3fbc..d3810c9 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,26 @@ # Mustache -[![godoc reference](https://godoc.org/github.com/alexkappa/mustache?status.svg)](https://godoc.org/github.com/alexkappa/mustache) -[![wercker status](https://app.wercker.com/status/37361276190f155a06df0c3f3e37a870/s/master "wercker status")](https://app.wercker.com/project/byKey/37361276190f155a06df0c3f3e37a870) -[![Code Climate](https://codeclimate.com/github/alexkappa/mustache/badges/gpa.svg)](https://codeclimate.com/github/alexkappa/mustache) -[![Test Coverage](https://api.codeclimate.com/v1/badges/6cd9f358f3e851036c22/test_coverage)](https://codeclimate.com/github/alexkappa/mustache/test_coverage) +[![Go Reference](https://pkg.go.dev/badge/github.com/alexkappa/mustache.svg)](https://pkg.go.dev/github.com/alexkappa/mustache) + +![](https://github.com/alexkappa/mustache/actions/workflows/go.yml/badge.svg) This is an implementation of the mustache templating language in Go. -It is inspired by [hoisie/mustache](https://github.com/hoisie/mustache) however it's not a fork, rather a re-implementation with improved spec conformance, a more flexible API (e.g. support for `io.Writer` and `io.Reader`). +It is inspired by [hoisie/mustache](https://github.com/hoisie/mustache) however +it's not a fork, but rather a re-implementation with improved spec conformance, +and a more flexible API (e.g. support for `io.Writer` and `io.Reader`). -It is built using lexing techniques described in the slides on [lexical scanning in Go](https://talks.golang.org/2011/lex.slide), and functional options as described in the blog post on [self-referential functions and the design of options](http://commandcenter.blogspot.nl/2014/01/self-referential-functions-and-design.html). +It is built using lexing techniques described in the slides on [lexical scanning +in Go](https://talks.golang.org/2011/lex.slide), and functional options as +described in the blog post on [self-referential functions and the design of +options](http://commandcenter.blogspot.nl/2014/01/self-referential-functions-and-design.html). -This package aims to cover 100% of the mustache specification tests, however by the time of this writing it is not complete. +This package aims to cover 100% of the mustache specification tests, however, by +the time of this writing, it is not complete. -For more information on mustache check the [official documentation](http://mustache.github.io/) and the [mustache spec](http://github.com/mustache/spec). +For more information on mustache check the [official +documentation](http://mustache.github.io/) and the [mustache +spec](http://github.com/mustache/spec). # Installation @@ -21,20 +28,24 @@ Install with `go get github.com/alexkappa/mustache`. # Documentation -The API documentation is available at [godoc.org](http://godoc.org/github.com/alexkappa/mustache). +The API documentation is available at +[godoc.org](https://pkg.go.dev/github.com/alexkappa/mustache). # Usage -The core of this package is the `Template`, and it's `Parse` and `Render` functions. +The core of this package is the `Template`, and its `Parse` and `Render` +functions. ```Go template := mustache.New() template.Parse(strings.NewReader("Hello, {{subject}}!")) template.Render(os.Stdout, map[string]string{"subject": "world"}) ``` + ## Helpers -There are additional `Parse` and `Render` helpers to deal with different kind of input or output, such as `string`, `[]byte` or `io.Writer`/`io.Reader`. +There are additional `Parse` and `Render` helpers to deal with different kinds +of input or output, such as `string`, `[]byte` or `io.Writer`/`io.Reader`. ```Go Parse(r io.Reader) error @@ -62,7 +73,9 @@ if err != nil { t.Render(os.Stdout, nil) ``` -**Note:** in the example above, we used [Parse](http://godoc.org/github.com/alexkappa/mustache#Parse) which wraps the `t := New()` and `t.Parse()` functions for consiceness. +**Note:** in the example above, we used +[Parse](https://pkg.go.dev/github.com/alexkappa/mustache#Parse) which wraps the +`t := New()` and `t.Parse()` functions for conciseness. ### String @@ -81,22 +94,33 @@ fmt.Println(s) ## Options -It is possible to define some options on the template, which will alter the way the template will parse, render or fail. +It is possible to define some options on the template, which will alter the way +the template will parse, render or fail. The options are: -- `Name(n string) Option` sets the name of the template. This option is useful when using the template as a partial to another template. -- `Delimiters(start, end string) Option` sets the start and end delimiters of the template. -- `Partial(p *Template) Option` sets p as a partial to the template. It is important to set the name of p so that it may be looked up by the parent template. -- `SilentMiss(silent bool) Option` sets missing variable lookup behaviour. +- `Name(n string) Option` sets the name of the template. This option is useful + when using the template as a partial to another template. +- `Delimiters(start, end string) Option` sets the start and end delimiters of + the template. +- `Partial(p *Template) Option` sets p as a partial to the template. It is + important to set the name of p so that it may be looked up by the parent + template. +- `SilentMiss(silent bool) Option` sets missing variable lookup behavior. -Options can be defined either as arguments to [New](http://godoc.org/github.com/alexkappa/mustache#New) or using the [Option](http://godoc.org/github.com/alexkappa/mustache#Template.Option) function. +Options can be defined either as arguments to +[New](https://pkg.go.dev/github.com/alexkappa/mustache#New) or using the +[Option](https://pkg.go.dev/github.com/alexkappa/mustache#Template.Option) +function. ## Partials -Partials are templates themselves and can be defined using the [Partial](http://godoc.org/github.com/alexkappa/mustache#Partial) option. +Partials are templates themselves and can be defined using the +[Partial](https://pkg.go.dev/github.com/alexkappa/mustache#Partial) option. -**Note:** It is important to name the partial using the [Name](http://godoc.org/github.com/alexkappa/mustache#Name) option which should match the mustache partial tag `{{>name}}` in the parent template. +**Note:** It is important to name the partial using the +[Name](https://pkg.go.dev/github.com/alexkappa/mustache#Name) option which should +match the mustache partial tag `{{>name}}` in the parent template. ```Go title := New( @@ -124,14 +148,57 @@ context := map[string]interface{}{ template.Render(os.Stdout, context) ``` +# Context + +When rendering, context can be either a `map` or a `struct`. Following are some +examples of valid context arguments. + +```Go +ctx := map[string]interface{}{ + "foo": "Hello", + "bar": map[string]string{ + "baz": "World", + } +} +mustache.Render("{{foo}} {{bar.baz}}", ctx) // Hello World +``` + +```Go +type Foo struct { Bar string } +ctx := &Foo{ Bar: "Hi, from a struct!" } +mustache.Render("{{Bar}}", ctx) // Hi, from a struct! +``` + +```Go +type Foo struct { bar string } +func (f *Foo) Bar() string { return f.bar } +ctx := &Foo{"Hi, from a method!"} +mustache.Render("{{Bar}}", ctx) // Hi, from a method! +``` + +```Go +type Foo struct { Bar string `tag:"bar"` } +ctx := &Foo{ Bar: "Hi, from a struct tag!" } +mustache.Render("{{bar}}", ctx) // Hi, from a struct tag! +``` + # Tests -Run `go test` as usual. If you want to run the spec tests against this package, make sure you've checked out the specs submodule. Otherwise spec tests will be skipped. +Run `go test` as usual. If you want to run the spec tests against this package, +make sure you've checked out the specs submodule. Otherwise, spec tests will be +skipped. -Currently certain spec tests are skipped as they fail due to an issue with how standalone tags and empty lines are being handled. Inspecting them manually, one can see that the templates render correctly but with some additional `\n` which should have been omited. See issue [#1](http://github.com/alexkappa/mustache/issues/1). +Currently, certain spec tests are skipped as they fail due to an issue with how +standalone tags and empty lines are being handled. Inspecting them manually, one +can see that the templates render correctly but with some additional `\n` which +should have been omitted. See issue +[#1](http://github.com/alexkappa/mustache/issues/1). -See [SPEC.md](https://github.com/alexkappa/mustache/blob/master/SPEC.md) for a breakdown of which spec tests pass and fail. +See [SPEC.md](https://github.com/alexkappa/mustache/blob/master/SPEC.md) for a +breakdown of which spec tests pass and fail. # Contributing -If you would like to contribute, head on to the [issues](https://github.com/alexkappa/mustache/issues) page for tasks that need help. +If you would like to contribute, head on to the +[issues](https://github.com/alexkappa/mustache/issues) page for tasks that need +help. diff --git a/lookup.go b/lookup.go index 763e8e8..03684b3 100644 --- a/lookup.go +++ b/lookup.go @@ -25,8 +25,9 @@ func lookup(name string, context ...interface{}) (interface{}, bool) { } // Iterate over the context chain and try to match the name to a value. for _, c := range context { - // Reflect on the value of the current context. + // Reflect on the value and type of the current context. reflectValue := reflect.ValueOf(c) + reflectType := reflect.TypeOf(c) // If the name is ".", we should return the whole context as-is. if name == "." { return c, truth(reflectValue) @@ -35,6 +36,10 @@ func lookup(name string, context ...interface{}) (interface{}, bool) { // If the current context is a map, we'll look for a key in that map // that matches the name. case reflect.Map: + // Try to match a map key to the name. For example: + // + // m := map[string]string{"foo": "bar"} + // mustache.Render("{{foo}}", m) item := reflectValue.MapIndex(reflect.ValueOf(name)) if item.IsValid() { return item.Interface(), truth(item) @@ -44,16 +49,45 @@ func lookup(name string, context ...interface{}) (interface{}, bool) { // support for matching struct names to tags so we can use lower_case // names in our templates which makes it more mustache like. case reflect.Struct: + // First we'll try to match a field. For example: + // + // type Foo struct { Bar string } + // ctx := &Foo{"baz"} + // mustache.Render("{{Bar}}", ctx) field := reflectValue.FieldByName(name) if field.IsValid() { return field.Interface(), truth(field) } + // If no field was matched, we'll try to match a method. This is + // useful for methods that return a value. For example: + // + // type Foo struct { bar string } + // func (f *Foo) Bar() string { return f.bar } + // ctx := &Foo{"baz"} + // mustache.Render("{{Bar}}", ctx) + // method := reflectValue.MethodByName(name) if method.IsValid() && method.Type().NumIn() == 1 { out := method.Call(nil)[0] return out.Interface(), truth(out) } - + // If no method was matched, we'll try to match a tag. This is + // useful for matching fields that have a different name than the + // one we want to use in our templates. For example: + // + // type Foo struct { + // Bar string `template:"baz"` + // } + // ctx := &Foo{"qux"} + // mustache.Render("{{baz}}", ctx) + // + for i := 0; i < reflectValue.NumField(); i++ { + field := reflectValue.Field(i) + tag := reflectType.Field(i).Tag.Get("template") + if tag == name { + return field.Interface(), truth(field) + } + } } // If by this point no value was matched, we'll move up a step in the // chain and try to match a value there. diff --git a/lookup_test.go b/lookup_test.go index eb58628..45b160f 100644 --- a/lookup_test.go +++ b/lookup_test.go @@ -56,6 +56,33 @@ func TestSimpleLookup(t *testing.T) { {"Nested.Inside", "I'm nested!", true}, }, }, + { + context: struct { + Integer int `template:"int"` + String string `template:"str"` + Boolean bool `template:"bool"` + Nested struct { + Inside string `template:"inside"` + } `template:"nested"` + }{ + Integer: 123, + String: "abc", + Boolean: true, + Nested: struct { + Inside string `template:"inside"` + }{"I'm nested!"}, + }, + assertions: []struct { + name string + value interface{} + truth bool + }{ + {"int", 123, true}, + {"str", "abc", true}, + {"bool", true, true}, + {"nested.inside", "I'm nested!", true}, + }, + }, } { for _, assertion := range test.assertions { value, truth := lookup(assertion.name, test.context)