diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bd49359 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: true + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20.4 + + - name: GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..1a9848e --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,28 @@ +name: Go Test + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: true + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20.4 + + - name: Test + run: make test + + - name: Upload results to codecov + uses: codecov/codecov-action@v3 + with: + verbose: true diff --git a/.gitignore b/.gitignore index 64bfa1d..f076d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ _obj _test _testmain.go *.swp +coverage.txt +/bin +/dist +/vendor diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..54b6158 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spec"] + path = spec + url = https://github.com/mustache/spec.git diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..f2ba801 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,13 @@ +env: + - GO111MODULE=on + - CGO_ENABLED=0 +builds: + - main: ./cmd/mustache + binary: mustache + goos: + - windows + - darwin + - linux + goarch: + - amd64 + - arm64 diff --git a/Makefile b/Makefile index 6313cf2..25d7a13 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,25 @@ +.PHONY: all +all: bin/mustache -GOFMT=gofmt -s -tabs=false -tabwidth=4 +.PHONY: clean +clean: + rm -rf bin -GOFILES=\ - mustache.go\ +.PHONY: test +test: + go test -race -coverprofile=coverage.txt -covermode=atomic ./... -format: - ${GOFMT} -w ${GOFILES} - ${GOFMT} -w mustache_test.go +.PHONY: fmt +fmt: + go fmt ./... +.PHONY: lint +lint: + golangci-lint run ./... + +SOURCES := $(shell find . -name '*.go') +BUILD_FLAGS ?= -v +LDFLAGS ?= -w -s + +bin/%: $(SOURCES) + CGO_ENABLED=0 go build -o $@ $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" ./cmd/$(@F) diff --git a/README.md b/README.md new file mode 100644 index 0000000..36bd899 --- /dev/null +++ b/README.md @@ -0,0 +1,233 @@ +# Mustache Template Engine for Go + +[![Build Status](https://img.shields.io/travis/cbroglie/mustache.svg)](https://travis-ci.org/cbroglie/mustache) +[![Go Doc](https://godoc.org/github.com/cbroglie/mustache?status.svg)](https://godoc.org/github.com/cbroglie/mustache) +[![Go Report Card](https://goreportcard.com/badge/github.com/cbroglie/mustache)](https://goreportcard.com/report/github.com/cbroglie/mustache) +[![codecov](https://codecov.io/gh/cbroglie/mustache/branch/master/graph/badge.svg)](https://codecov.io/gh/cbroglie/mustache) +[![Downloads](https://img.shields.io/github/downloads/cbroglie/mustache/latest/total.svg)](https://github.com/cbroglie/mustache/releases) +[![Latest release](https://img.shields.io/github/release/cbroglie/mustache.svg)](https://github.com/cbroglie/mustache/releases) + + +logo + +---- + +## Why a Fork? + +I forked [hoisie/mustache](https://github.com/hoisie/mustache) because it does not appear to be maintained, and I wanted to add the following functionality: + +- Update the API to follow the idiomatic Go convention of returning errors (this is a breaking change) +- Add option to treat missing variables as errors + +---- + +## CLI Overview + +```bash +➜ ~ go install github.com/cbroglie/mustache/cmd/mustache@latest +➜ ~ mustache +Usage: + mustache [data] template [flags] + +Examples: + $ mustache data.yml template.mustache + $ cat data.yml | mustache template.mustache + $ mustache --layout wrapper.mustache data template.mustache + $ mustache --overide over.yml data.yml template.mustache + +Flags: + -h, --help help for mustache + --layout a file to use as the layout template + --override a data.yml file whose definitions supercede data.yml +➜ ~ +``` + +---- + +## Package Overview + +This library is an implementation of the Mustache template language in Go. + +### Mustache Spec Compliance + +[mustache/spec](https://github.com/mustache/spec) contains the formal standard for Mustache, and it is included as a submodule (using v1.2.1) for testing compliance. All of the tests pass (big thanks to [kei10in](https://github.com/kei10in)), with the exception of the null interpolation tests added in v1.2.1. There is experimental support for a subset of the optional lambda functionality (thanks to [fromhut](https://github.com/fromhut)). The optional inheritance functionality has not been implemented. + +---- + +## Documentation + +For more information about mustache, check out the [mustache project page](https://github.com/mustache/mustache) or the [mustache manual](https://mustache.github.io/mustache.5.html). + +Also check out some [example mustache files](http://github.com/mustache/mustache/tree/master/examples/). + +---- + +## Installation + +To install the CLI, run `go install github.com/cbroglie/mustache/cmd/mustache@latest`. To use it in a program, run `go get github.com/cbroglie/mustache` and use `import "github.com/cbroglie/mustache"`. + +---- + +## Usage + +There are four main methods in this package: + +```go +Render(data string, context ...interface{}) (string, error) + +RenderFile(filename string, context ...interface{}) (string, error) + +ParseString(data string) (*Template, error) + +ParseFile(filename string) (*Template, error) +``` + +There are also two additional methods for using layouts (explained below); as well as several more that can provide a custom Partial retrieval. + +The Render method takes a string and a data source, which is generally a map or struct, and returns the output string. If the template file contains an error, the return value is a description of the error. There's a similar method, RenderFile, which takes a filename as an argument and uses that for the template contents. + +```go +data, err := mustache.Render("hello {{c}}", map[string]string{"c": "world"}) +``` + +If you're planning to render the same template multiple times, you do it efficiently by compiling the template first: + +```go +tmpl, _ := mustache.ParseString("hello {{c}}") +var buf bytes.Buffer +for i := 0; i < 10; i++ { + tmpl.FRender(&buf, map[string]string{"c": "world"}) +} +``` + +For more example usage, please see `mustache_test.go` + +---- + +## Escaping + +mustache.go follows the official mustache HTML escaping rules. That is, if you enclose a variable with two curly brackets, `{{var}}`, the contents are HTML-escaped. For instance, strings like `5 > 2` are converted to `5 > 2`. To use raw characters, use three curly brackets `{{{var}}}`. + +---- + +## Layouts + +It is a common pattern to include a template file as a "wrapper" for other templates. The wrapper may include a header and a footer, for instance. Mustache.go supports this pattern with the following two methods: + +```go +RenderInLayout(data string, layout string, context ...interface{}) (string, error) + +RenderFileInLayout(filename string, layoutFile string, context ...interface{}) (string, error) +``` + +The layout file must have a variable called `{{content}}`. For example, given the following files: + +layout.html.mustache: + +```html + +Hi + +{{{content}}} + + +``` + +template.html.mustache: + +```html +

Hello World!

+``` + +A call to `RenderFileInLayout("template.html.mustache", "layout.html.mustache", nil)` will produce: + +```html + +Hi + +

Hello World!

+ + +``` + +---- + +## Custom PartialProvider + +Mustache.go has been extended to support a user-defined repository for mustache partials, instead of the default of requiring file-based templates. + +Several new top-level functions have been introduced to take advantage of this: + +```go + +func RenderPartials(data string, partials PartialProvider, context ...interface{}) (string, error) + +func RenderInLayoutPartials(data string, layoutData string, partials PartialProvider, context ...interface{}) (string, error) + +func ParseStringPartials(data string, partials PartialProvider) (*Template, error) + +func ParseFilePartials(filename string, partials PartialProvider) (*Template, error) + +``` + +A `PartialProvider` is any object that responds to `Get(string) +(*Template,error)`, and two examples are provided- a `FileProvider` that +recreates the old behavior (and is indeed used internally for backwards +compatibility); and a `StaticProvider` alias for a `map[string]string`. Using +either of these is simple: + +```go + +fp := &FileProvider{ + Paths: []string{ "", "/opt/mustache", "templates/" }, + Extensions: []string{ "", ".stache", ".mustache" }, +} + +tmpl, err := ParseStringPartials("This partial is loaded from a file: {{>foo}}", fp) + +sp := StaticProvider(map[string]string{ + "foo": "{{>bar}}", + "bar": "some data", +}) + +tmpl, err := ParseStringPartials("This partial is loaded from a map: {{>foo}}", sp) +``` + +---- + +## A note about method receivers + +Mustache.go supports calling methods on objects, but you have to be aware of Go's limitations. For example, lets's say you have the following type: + +```go +type Person struct { + FirstName string + LastName string +} + +func (p *Person) Name1() string { + return p.FirstName + " " + p.LastName +} + +func (p Person) Name2() string { + return p.FirstName + " " + p.LastName +} +``` + +While they appear to be identical methods, `Name1` has a pointer receiver, and `Name2` has a value receiver. Objects of type `Person`(non-pointer) can only access `Name2`, while objects of type `*Person`(person) can access both. This is by design in the Go language. + +So if you write the following: + +```go +mustache.Render("{{Name1}}", Person{"John", "Smith"}) +``` + +It'll be blank. You either have to use `&Person{"John", "Smith"}`, or call `Name2` + +## Supported features + +- Variables +- Comments +- Change delimiter +- Sections (boolean, enumerable, and inverted) +- Partials diff --git a/Readme.md b/Readme.md deleted file mode 100644 index e9a0f52..0000000 --- a/Readme.md +++ /dev/null @@ -1,112 +0,0 @@ -## Overview - -mustache.go is an implementation of the mustache template language in Go. It is better suited for website templates than Go's native pkg/template. mustache.go is fast -- it parses templates efficiently and stores them in a tree-like structure which allows for fast execution. - -## Documentation - -For more information about mustache, check out the [mustache project page](http://github.com/defunkt/mustache) or the [mustache manual](http://mustache.github.com/mustache.5.html). - -Also check out some [example mustache files](http://github.com/defunkt/mustache/tree/master/examples/) - -## Installation -To install mustache.go, simply run `go get github.com/hoisie/mustache`. To use it in a program, use `import "github.com/hoisie/mustache"` - -## Usage -There are four main methods in this package: - - func Render(data string, context ...interface{}) string - - func RenderFile(filename string, context ...interface{}) string - - func ParseString(data string) (*Template, os.Error) - - func ParseFile(filename string) (*Template, os.Error) - -There are also two additional methods for using layouts (explained below). - -The Render method takes a string and a data source, which is generally a map or struct, and returns the output string. If the template file contains an error, the return value is a description of the error. There's a similar method, RenderFile, which takes a filename as an argument and uses that for the template contents. - - data := mustache.Render("hello {{c}}", map[string]string{"c":"world"}) - println(data) - - -If you're planning to render the same template multiple times, you do it efficiently by compiling the template first: - - tmpl,_ := mustache.ParseString("hello {{c}}") - var buf bytes.Buffer; - for i := 0; i < 10; i++ { - tmpl.Render (map[string]string { "c":"world"}, &buf) - } - -For more example usage, please see `mustache_test.go` - -## Escaping - -mustache.go follows the official mustache HTML escaping rules. That is, if you enclose a variable with two curly brackets, `{{var}}`, the contents are HTML-escaped. For instance, strings like `5 > 2` are converted to `5 > 2`. To use raw characters, use three curly brackets `{{{var}}}`. - -## Layouts - -It is a common pattern to include a template file as a "wrapper" for other templates. The wrapper may include a header and a footer, for instance. Mustache.go supports this pattern with the following two methods: - - func RenderInLayout(data string, layout string, context ...interface{}) string - - func RenderFileInLayout(filename string, layoutFile string, context ...interface{}) string - -The layout file must have a variable called `{{content}}`. For example, given the following files: - -layout.html.mustache: - - - Hi - - {{{content}}} - - - -template.html.mustache: - -

Hello World!

- -A call to `RenderFileInLayout("template.html.mustache", "layout.html.mustache", nil)` will produce: - - - Hi - -

Hello World!

- - - -## A note about method receivers - -Mustache.go supports calling methods on objects, but you have to be aware of Go's limitations. For example, lets's say you have the following type: - - type Person struct { - FirstName string - LastName string - } - - func (p *Person) Name1() string { - return p.FirstName + " " + p.LastName - } - - func (p Person) Name2() string { - return p.FirstName + " " + p.LastName - } - -While they appear to be identical methods, `Name1` has a pointer receiver, and `Name2` has a value receiver. Objects of type `Person`(non-pointer) can only access `Name2`, while objects of type `*Person`(person) can access both. This is by design in the Go language. - -So if you write the following: - - mustache.Render("{{Name1}}", Person{"John", "Smith"}) - -It'll be blank. You either have to use `&Person{"John", "Smith"}`, or call `Name2` - -## Supported features - -* Variables -* Comments -* Change delimiter -* Sections (boolean, enumerable, and inverted) -* Partials - - diff --git a/cmd/mustache/.gitignore b/cmd/mustache/.gitignore new file mode 100644 index 0000000..12c0645 --- /dev/null +++ b/cmd/mustache/.gitignore @@ -0,0 +1 @@ +mustache \ No newline at end of file diff --git a/cmd/mustache/main.go b/cmd/mustache/main.go new file mode 100644 index 0000000..325c2dd --- /dev/null +++ b/cmd/mustache/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + + "github.com/cbroglie/mustache" +) + +var rootCmd = &cobra.Command{ + Use: "mustache [--layout template] [data] template", + Example: ` $ mustache data.yml template.mustache + $ cat data.yml | mustache template.mustache + $ mustache --layout wrapper.mustache data template.mustache + $ mustache --override over.yml data.yml template.mustache`, + Args: cobra.RangeArgs(0, 2), + Run: func(cmd *cobra.Command, args []string) { + err := run(cmd, args) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + os.Exit(1) + } + }, +} +var layoutFile string +var overrideFile string + +func main() { + rootCmd.Flags().StringVar(&layoutFile, "layout", "", "location of layout file") + rootCmd.Flags().StringVar(&overrideFile, "override", "", "location of data.yml override yml") + rootCmd.Flags().BoolVar(&mustache.AllowMissingVariables, "allow-missing-variables", true, "allow missing variables") + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func run(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Usage() + } + + var data interface{} + var templatePath string + if len(args) == 1 { + var err error + data, err = parseDataFromStdIn() + if err != nil { + return err + } + templatePath = args[0] + } else { + var err error + data, err = parseDataFromFile(args[0]) + if err != nil { + return err + } + templatePath = args[1] + } + + if overrideFile != "" { + override, err := parseDataFromFile(overrideFile) + if err != nil { + return err + } + for k, v := range override.(map[interface{}]interface{}) { + data.(map[interface{}]interface{})[k] = v + } + } + var output string + var err error + if layoutFile != "" { + output, err = mustache.RenderFileInLayout(templatePath, layoutFile, data) + } else { + output, err = mustache.RenderFile(templatePath, data) + } + if err != nil { + return err + } + fmt.Print(output) + return nil +} + +func parseDataFromStdIn() (interface{}, error) { + b, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, err + } + var data interface{} + if err := yaml.Unmarshal(b, &data); err != nil { + return nil, err + } + return data, nil +} + +func parseDataFromFile(filePath string) (interface{}, error) { + b, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + var data interface{} + if err := yaml.Unmarshal(b, &data); err != nil { + return nil, err + } + return data, nil +} 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/go.mod b/go.mod new file mode 100644 index 0000000..93eb093 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/cbroglie/mustache + +go 1.20 + +require ( + github.com/spf13/cobra v1.7.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e823c9d --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/images/logo.jpeg b/images/logo.jpeg new file mode 100644 index 0000000..6b734ad Binary files /dev/null and b/images/logo.jpeg differ diff --git a/mustache.go b/mustache.go index de82430..e268b80 100644 --- a/mustache.go +++ b/mustache.go @@ -1,599 +1,956 @@ package mustache import ( - "bytes" - "errors" - "fmt" - "html/template" - "io" - "io/ioutil" - "os" - "path" - "reflect" - "strings" + "bytes" + "fmt" + "html/template" + "io" + "os" + "path" + "reflect" + "strconv" + "strings" ) +var ( + // AllowMissingVariables defines the behavior for a variable "miss." If it + // is true (the default), an empty string is emitted. If it is false, an error + // is generated instead. + AllowMissingVariables = true +) + +// RenderFunc is provided to lambda functions for rendering. +type RenderFunc func(text string) (string, error) + +// LambdaFunc is the signature for lambda functions. +type LambdaFunc func(text string, render RenderFunc) (string, error) + +// EscapeFunc is used for escaping non-raw values in templates. +type EscapeFunc func(text string) string + +// A TagType represents the specific type of mustache tag that a Tag +// represents. The zero TagType is not a valid type. +type TagType uint + +// Defines representing the possible Tag types +const ( + Invalid TagType = iota + Variable + Section + InvertedSection + Partial +) + +// Skip all whitespaces apeared after these types of tags until end of line +// if the line only contains a tag and whitespaces. +const ( + SkipWhitespaceTagTypes = "#^/<>=!" +) + +func (t TagType) String() string { + if int(t) < len(tagNames) { + return tagNames[t] + } + return "type" + strconv.Itoa(int(t)) +} + +var tagNames = []string{ + Invalid: "Invalid", + Variable: "Variable", + Section: "Section", + InvertedSection: "InvertedSection", + Partial: "Partial", +} + +// Tag represents the different mustache tag types. +// +// Not all methods apply to all kinds of tags. Restrictions, if any, are noted +// in the documentation for each method. Use the Type method to find out the +// type of tag before calling type-specific methods. Calling a method +// inappropriate to the type of tag causes a run time panic. +type Tag interface { + // Type returns the type of the tag. + Type() TagType + // Name returns the name of the tag. + Name() string + // Tags returns any child tags. It panics for tag types which cannot contain + // child tags (i.e. variable tags). + Tags() []Tag +} + type textElement struct { - text []byte + text []byte } type varElement struct { - name string - raw bool + name string + raw bool } type sectionElement struct { - name string - inverted bool - startline int - elems []interface{} + name string + inverted bool + startline int + elems []interface{} } +type partialElement struct { + name string + indent string + prov PartialProvider +} + +// Template represents a compilde mustache template type Template struct { - data string - otag string - ctag string - p int - curline int - dir string - elems []interface{} + data string + otag string + ctag string + p int + curline int + elems []interface{} + forceRaw bool + partial PartialProvider + escape EscapeFunc } -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) } -func (p parseError) Error() string { return fmt.Sprintf("line %d: %s", p.line, p.message) } +// Escape sets custom escape function. By-default it is HTMLEscape. +func (tmpl *Template) Escape(fn EscapeFunc) { + tmpl.escape = fn +} -var ( - esc_quot = []byte(""") - esc_apos = []byte("'") - esc_amp = []byte("&") - esc_lt = []byte("<") - esc_gt = []byte(">") -) +func extractTags(elems []interface{}) []Tag { + tags := make([]Tag, 0, len(elems)) + for _, elem := range elems { + switch elem := elem.(type) { + case *varElement: + tags = append(tags, elem) + case *sectionElement: + tags = append(tags, elem) + case *partialElement: + tags = append(tags, elem) + } + } + return tags +} + +func (e *varElement) Type() TagType { + return Variable +} + +func (e *varElement) Name() string { + return e.name +} + +func (e *varElement) Tags() []Tag { + panic("mustache: Tags on Variable type") +} + +func (e *sectionElement) Type() TagType { + if e.inverted { + return InvertedSection + } + return Section +} + +func (e *sectionElement) Name() string { + return e.name +} + +func (e *sectionElement) Tags() []Tag { + return extractTags(e.elems) +} + +func (e *partialElement) Type() TagType { + return Partial +} + +func (e *partialElement) Name() string { + return e.name +} + +func (e *partialElement) Tags() []Tag { + return nil +} func (tmpl *Template) readString(s string) (string, error) { - i := tmpl.p - newlines := 0 - for true { - //are we at the end of the string? - if i+len(s) > len(tmpl.data) { - return tmpl.data[tmpl.p:], io.EOF - } - - if tmpl.data[i] == '\n' { - newlines++ - } - - if tmpl.data[i] != s[0] { - i++ - continue - } - - match := true - for j := 1; j < len(s); j++ { - if s[j] != tmpl.data[i+j] { - match = false - break - } - } - - if match { - e := i + len(s) - text := tmpl.data[tmpl.p:e] - tmpl.p = e - - tmpl.curline += newlines - return text, nil - } else { - i++ - } - } - - //should never be here - return "", nil -} - -func (tmpl *Template) parsePartial(name string) (*Template, error) { - filenames := []string{ - path.Join(tmpl.dir, name), - path.Join(tmpl.dir, name+".mustache"), - path.Join(tmpl.dir, name+".stache"), - name, - name + ".mustache", - name + ".stache", - } - var filename string - for _, name := range filenames { - f, err := os.Open(name) - if err == nil { - filename = name - f.Close() - break - } - } - if filename == "" { - return nil, errors.New(fmt.Sprintf("Could not find partial %q", name)) - } - - partial, err := ParseFile(filename) - - if err != nil { - return nil, err - } - - return partial, nil + newlines := 0 + for i := tmpl.p; ; i++ { + //are we at the end of the string? + if i+len(s) > len(tmpl.data) { + return tmpl.data[tmpl.p:], io.EOF + } + + if tmpl.data[i] == '\n' { + newlines++ + } + + if tmpl.data[i] != s[0] { + continue + } + + match := true + for j := 1; j < len(s); j++ { + if s[j] != tmpl.data[i+j] { + match = false + break + } + } + + if match { + e := i + len(s) + text := tmpl.data[tmpl.p:e] + tmpl.p = e + + tmpl.curline += newlines + return text, nil + } + } +} + +type textReadingResult struct { + text string + padding string + mayStandalone bool +} + +func (tmpl *Template) readText() (*textReadingResult, error) { + pPrev := tmpl.p + text, err := tmpl.readString(tmpl.otag) + if err == io.EOF { + return &textReadingResult{ + text: text, + padding: "", + mayStandalone: false, + }, err + } + + var i int + for i = tmpl.p - len(tmpl.otag); i > pPrev; i-- { + if tmpl.data[i-1] != ' ' && tmpl.data[i-1] != '\t' { + break + } + } + + mayStandalone := (i == 0 || tmpl.data[i-1] == '\n') + + if mayStandalone { + return &textReadingResult{ + text: tmpl.data[pPrev:i], + padding: tmpl.data[i : tmpl.p-len(tmpl.otag)], + mayStandalone: true, + }, nil + } + + return &textReadingResult{ + text: tmpl.data[pPrev : tmpl.p-len(tmpl.otag)], + padding: "", + mayStandalone: false, + }, nil +} + +type tagReadingResult struct { + tag string + standalone bool +} + +func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) { + var text string + var err error + if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' { + text, err = tmpl.readString("}" + tmpl.ctag) + } else { + text, err = tmpl.readString(tmpl.ctag) + } + + if err == io.EOF { + //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 + tag := strings.TrimSpace(text) + if len(tag) == 0 { + return nil, newError(tmpl.curline, ErrEmptyTag) + } + + eow := tmpl.p + for i := tmpl.p; i < len(tmpl.data); i++ { + if !(tmpl.data[i] == ' ' || tmpl.data[i] == '\t') { + eow = i + break + } + } + + standalone := true + if mayStandalone { + if !strings.Contains(SkipWhitespaceTagTypes, tag[0:1]) { + standalone = false + } else { + if eow == len(tmpl.data) { + standalone = true + tmpl.p = eow + } else if eow < len(tmpl.data) && tmpl.data[eow] == '\n' { + standalone = true + tmpl.p = eow + 1 + tmpl.curline++ + } else if eow+1 < len(tmpl.data) && tmpl.data[eow] == '\r' && tmpl.data[eow+1] == '\n' { + standalone = true + tmpl.p = eow + 2 + tmpl.curline++ + } else { + standalone = false + } + } + } + + return &tagReadingResult{ + tag: tag, + standalone: standalone, + }, nil +} + +func (tmpl *Template) parsePartial(name, indent string) (*partialElement, error) { + return &partialElement{ + name: name, + indent: indent, + prov: tmpl.partial, + }, nil } func (tmpl *Template) parseSection(section *sectionElement) error { - for { - text, err := tmpl.readString(tmpl.otag) - - if err == io.EOF { - return parseError{section.startline, "Section " + section.name + " has no closing tag"} - } - - // put text into an item - text = text[0 : len(text)-len(tmpl.otag)] - section.elems = append(section.elems, &textElement{[]byte(text)}) - if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' { - text, err = tmpl.readString("}" + tmpl.ctag) - } else { - text, err = tmpl.readString(tmpl.ctag) - } - - if err == io.EOF { - //put the remaining text in a block - return parseError{tmpl.curline, "unmatched open tag"} - } - - //trim the close tag off the text - tag := strings.TrimSpace(text[0 : len(text)-len(tmpl.ctag)]) - - if len(tag) == 0 { - return parseError{tmpl.curline, "empty tag"} - } - switch tag[0] { - case '!': - //ignore comment - break - case '#', '^': - name := strings.TrimSpace(tag[1:]) - - //ignore the newline when a section starts - if len(tmpl.data) > tmpl.p && tmpl.data[tmpl.p] == '\n' { - tmpl.p += 1 - } else if len(tmpl.data) > tmpl.p+1 && tmpl.data[tmpl.p] == '\r' && tmpl.data[tmpl.p+1] == '\n' { - tmpl.p += 2 - } - - se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}} - err := tmpl.parseSection(&se) - if err != nil { - return err - } - section.elems = append(section.elems, &se) - case '/': - name := strings.TrimSpace(tag[1:]) - if name != section.name { - return parseError{tmpl.curline, "interleaved closing tag: " + name} - } else { - return nil - } - case '>': - name := strings.TrimSpace(tag[1:]) - partial, err := tmpl.parsePartial(name) - if err != nil { - return err - } - section.elems = append(section.elems, partial) - case '=': - if tag[len(tag)-1] != '=' { - return parseError{tmpl.curline, "Invalid meta tag"} - } - tag = strings.TrimSpace(tag[1 : len(tag)-1]) - newtags := strings.SplitN(tag, " ", 2) - if len(newtags) == 2 { - tmpl.otag = newtags[0] - tmpl.ctag = newtags[1] - } - case '{': - if tag[len(tag)-1] == '}' { - //use a raw tag - section.elems = append(section.elems, &varElement{tag[1 : len(tag)-1], true}) - } - default: - section.elems = append(section.elems, &varElement{tag, false}) - } - } - - return nil + for { + textResult, err := tmpl.readText() + text := textResult.text + padding := textResult.padding + mayStandalone := textResult.mayStandalone + + if err == io.EOF { + //put the remaining text in a block + return newErrorWithReason(section.startline, ErrSectionNoClosingTag, section.name) + } + + // put text into an item + section.elems = append(section.elems, &textElement{[]byte(text)}) + + tagResult, err := tmpl.readTag(mayStandalone) + if err != nil { + return err + } + + if !tagResult.standalone { + section.elems = append(section.elems, &textElement{[]byte(padding)}) + } + + tag := tagResult.tag + switch tag[0] { + case '!': + //ignore comment + case '#', '^': + name := strings.TrimSpace(tag[1:]) + se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}} + err := tmpl.parseSection(&se) + if err != nil { + return err + } + section.elems = append(section.elems, &se) + case '/': + name := strings.TrimSpace(tag[1:]) + if name != section.name { + return newErrorWithReason(tmpl.curline, ErrInterleavedClosingTag, name) + } + return nil + case '>': + name := strings.TrimSpace(tag[1:]) + partial, err := tmpl.parsePartial(name, textResult.padding) + if err != nil { + return err + } + section.elems = append(section.elems, partial) + case '=': + if tag[len(tag)-1] != '=' { + return newError(tmpl.curline, ErrInvalidMetaTag) + } + tag = strings.TrimSpace(tag[1 : len(tag)-1]) + newtags := strings.SplitN(tag, " ", 2) + if len(newtags) == 2 { + tmpl.otag = newtags[0] + tmpl.ctag = newtags[1] + } + case '{': + if tag[len(tag)-1] == '}' { + //use a raw tag + name := strings.TrimSpace(tag[1 : len(tag)-1]) + section.elems = append(section.elems, &varElement{name, true}) + } + case '&': + name := strings.TrimSpace(tag[1:]) + section.elems = append(section.elems, &varElement{name, true}) + default: + section.elems = append(section.elems, &varElement{tag, tmpl.forceRaw}) + } + } } func (tmpl *Template) parse() error { - for { - text, err := tmpl.readString(tmpl.otag) - if err == io.EOF { - //put the remaining text in a block - tmpl.elems = append(tmpl.elems, &textElement{[]byte(text)}) - return nil - } - - // put text into an item - text = text[0 : len(text)-len(tmpl.otag)] - tmpl.elems = append(tmpl.elems, &textElement{[]byte(text)}) - - if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' { - text, err = tmpl.readString("}" + tmpl.ctag) - } else { - text, err = tmpl.readString(tmpl.ctag) - } - - if err == io.EOF { - //put the remaining text in a block - return parseError{tmpl.curline, "unmatched open tag"} - } - - //trim the close tag off the text - tag := strings.TrimSpace(text[0 : len(text)-len(tmpl.ctag)]) - if len(tag) == 0 { - return parseError{tmpl.curline, "empty tag"} - } - switch tag[0] { - case '!': - //ignore comment - break - case '#', '^': - name := strings.TrimSpace(tag[1:]) - - if len(tmpl.data) > tmpl.p && tmpl.data[tmpl.p] == '\n' { - tmpl.p += 1 - } else if len(tmpl.data) > tmpl.p+1 && tmpl.data[tmpl.p] == '\r' && tmpl.data[tmpl.p+1] == '\n' { - tmpl.p += 2 - } - - se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}} - err := tmpl.parseSection(&se) - if err != nil { - return err - } - tmpl.elems = append(tmpl.elems, &se) - case '/': - return parseError{tmpl.curline, "unmatched close tag"} - case '>': - name := strings.TrimSpace(tag[1:]) - partial, err := tmpl.parsePartial(name) - if err != nil { - return err - } - tmpl.elems = append(tmpl.elems, partial) - case '=': - if tag[len(tag)-1] != '=' { - return parseError{tmpl.curline, "Invalid meta tag"} - } - tag = strings.TrimSpace(tag[1 : len(tag)-1]) - newtags := strings.SplitN(tag, " ", 2) - if len(newtags) == 2 { - tmpl.otag = newtags[0] - tmpl.ctag = newtags[1] - } - case '{': - //use a raw tag - if tag[len(tag)-1] == '}' { - tmpl.elems = append(tmpl.elems, &varElement{tag[1 : len(tag)-1], true}) - } - default: - tmpl.elems = append(tmpl.elems, &varElement{tag, false}) - } - } - - return nil -} - -// See if name is a method of the value at some level of indirection. -// The return values are the result of the call (which may be nil if -// there's trouble) and whether a method of the right name exists with -// any signature. -func callMethod(data reflect.Value, name string) (result reflect.Value, found bool) { - found = false - // Method set depends on pointerness, and the value may be arbitrarily - // indirect. Simplest approach is to walk down the pointer chain and - // see if we can find the method at each step. - // Most steps will see NumMethod() == 0. - for { - typ := data.Type() - if nMethod := data.Type().NumMethod(); nMethod > 0 { - for i := 0; i < nMethod; i++ { - method := typ.Method(i) - if method.Name == name { - - found = true // we found the name regardless - // does receiver type match? (pointerness might be off) - if typ == method.Type.In(0) { - return call(data, method), found - } - } - } - } - if nd := data; nd.Kind() == reflect.Ptr { - data = nd.Elem() - } else { - break - } - } - return -} - -// Invoke the method. If its signature is wrong, return nil. -func call(v reflect.Value, method reflect.Method) reflect.Value { - funcType := method.Type - // Method must take no arguments, meaning as a func it has one argument (the receiver) - if funcType.NumIn() != 1 { - return reflect.Value{} - } - // Method must return a single value. - if funcType.NumOut() == 0 { - return reflect.Value{} - } - // Result will be the zeroth element of the returned slice. - return method.Func.Call([]reflect.Value{v})[0] + for { + textResult, err := tmpl.readText() + text := textResult.text + padding := textResult.padding + mayStandalone := textResult.mayStandalone + + if err == io.EOF { + //put the remaining text in a block + tmpl.elems = append(tmpl.elems, &textElement{[]byte(text)}) + return nil + } + + // put text into an item + tmpl.elems = append(tmpl.elems, &textElement{[]byte(text)}) + + tagResult, err := tmpl.readTag(mayStandalone) + if err != nil { + return err + } + + if !tagResult.standalone { + tmpl.elems = append(tmpl.elems, &textElement{[]byte(padding)}) + } + + tag := tagResult.tag + switch tag[0] { + case '!': + //ignore comment + case '#', '^': + name := strings.TrimSpace(tag[1:]) + se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}} + err := tmpl.parseSection(&se) + if err != nil { + return err + } + tmpl.elems = append(tmpl.elems, &se) + case '/': + return newError(tmpl.curline, ErrUnmatchedCloseTag) + case '>': + name := strings.TrimSpace(tag[1:]) + partial, err := tmpl.parsePartial(name, textResult.padding) + if err != nil { + return err + } + tmpl.elems = append(tmpl.elems, partial) + case '=': + if tag[len(tag)-1] != '=' { + return newError(tmpl.curline, ErrInvalidMetaTag) + } + tag = strings.TrimSpace(tag[1 : len(tag)-1]) + newtags := strings.SplitN(tag, " ", 2) + if len(newtags) == 2 { + tmpl.otag = newtags[0] + tmpl.ctag = newtags[1] + } + case '{': + //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}) + } + case '&': + name := strings.TrimSpace(tag[1:]) + tmpl.elems = append(tmpl.elems, &varElement{name, true}) + default: + tmpl.elems = append(tmpl.elems, &varElement{tag, tmpl.forceRaw}) + } + } } // Evaluate interfaces and pointers looking for a value that can look up the name, via a // struct field, method, or map key, and return the result of the lookup. -func lookup(contextChain []interface{}, name string) reflect.Value { - // dot notation - if name != "." && strings.Contains(name, ".") { - parts := strings.SplitN(name, ".", 2) - - v := lookup(contextChain, parts[0]) - return lookup([]interface{}{v}, parts[1]) - } - - defer func() { - if r := recover(); r != nil { - fmt.Printf("Panic while looking up %q: %s\n", name, r) - } - }() +func lookup(contextChain []interface{}, name string, allowMissing bool) (reflect.Value, error) { + // dot notation + if name != "." && strings.Contains(name, ".") { + parts := strings.SplitN(name, ".", 2) + + v, err := lookup(contextChain, parts[0], allowMissing) + if err != nil { + return v, err + } + return lookup([]interface{}{v}, parts[1], allowMissing) + } + + defer func() { + if r := recover(); r != nil { + fmt.Printf("Panic while looking up %q: %s\n", name, r) + } + }() Outer: - for _, ctx := range contextChain { //i := len(contextChain) - 1; i >= 0; i-- { - v := ctx.(reflect.Value) - for v.IsValid() { - typ := v.Type() - if n := v.Type().NumMethod(); n > 0 { - for i := 0; i < n; i++ { - m := typ.Method(i) - mtyp := m.Type - if m.Name == name && mtyp.NumIn() == 1 { - return v.Method(i).Call(nil)[0] - } - } - } - if name == "." { - return v - } - switch av := v; av.Kind() { - case reflect.Ptr: - v = av.Elem() - case reflect.Interface: - v = av.Elem() - case reflect.Struct: - ret := av.FieldByName(name) - if ret.IsValid() { - return ret - } else { - continue Outer - } - case reflect.Map: - ret := av.MapIndex(reflect.ValueOf(name)) - if ret.IsValid() { - return ret - } else { - continue Outer - } - default: - continue Outer - } - } - } - return reflect.Value{} + for _, ctx := range contextChain { + v := ctx.(reflect.Value) + for v.IsValid() { + typ := v.Type() + if n := v.Type().NumMethod(); n > 0 { + for i := 0; i < n; i++ { + m := typ.Method(i) + mtyp := m.Type + if m.Name == name && mtyp.NumIn() == 1 { + return v.Method(i).Call(nil)[0], nil + } + } + } + if name == "." { + return v, nil + } + switch av := v; av.Kind() { + case reflect.Ptr: + v = av.Elem() + case reflect.Interface: + v = av.Elem() + case reflect.Struct: + ret := av.FieldByName(name) + if ret.IsValid() { + return ret, nil + } + continue Outer + case reflect.Map: + ret := av.MapIndex(reflect.ValueOf(name)) + if ret.IsValid() { + return ret, nil + } + continue Outer + default: + continue Outer + } + } + } + if allowMissing { + return reflect.Value{}, nil + } + return reflect.Value{}, fmt.Errorf("missing variable %q", name) } func isEmpty(v reflect.Value) bool { - if !v.IsValid() || v.Interface() == nil { - return true - } - - valueInd := indirect(v) - if !valueInd.IsValid() { - return true - } - switch val := valueInd; val.Kind() { - case reflect.Bool: - return !val.Bool() - case reflect.Slice: - return val.Len() == 0 - } - - return false + if !v.IsValid() || v.Interface() == nil { + return true + } + + valueInd := indirect(v) + if !valueInd.IsValid() { + return true + } + switch val := valueInd; val.Kind() { + case reflect.Array, reflect.Slice: + return val.Len() == 0 + case reflect.String: + return len(strings.TrimSpace(val.String())) == 0 + default: + return valueInd.IsZero() + } } func indirect(v reflect.Value) reflect.Value { loop: - for v.IsValid() { - switch av := v; av.Kind() { - case reflect.Ptr: - v = av.Elem() - case reflect.Interface: - v = av.Elem() - default: - break loop - } - } - return v -} - -func renderSection(section *sectionElement, contextChain []interface{}, buf io.Writer) { - value := lookup(contextChain, section.name) - var context = contextChain[len(contextChain)-1].(reflect.Value) - var contexts = []interface{}{} - // if the value is nil, check if it's an inverted section - isEmpty := isEmpty(value) - if isEmpty && !section.inverted || !isEmpty && section.inverted { - return - } else if !section.inverted { - valueInd := indirect(value) - switch val := valueInd; val.Kind() { - case reflect.Slice: - for i := 0; i < val.Len(); i++ { - contexts = append(contexts, val.Index(i)) - } - case reflect.Array: - for i := 0; i < val.Len(); i++ { - contexts = append(contexts, val.Index(i)) - } - case reflect.Map, reflect.Struct: - contexts = append(contexts, value) - default: - contexts = append(contexts, context) - } - } else if section.inverted { - contexts = append(contexts, context) - } - - chain2 := make([]interface{}, len(contextChain)+1) - copy(chain2[1:], contextChain) - //by default we execute the section - for _, ctx := range contexts { - chain2[0] = ctx - for _, elem := range section.elems { - renderElement(elem, chain2, buf) - } - } -} - -func renderElement(element interface{}, contextChain []interface{}, buf io.Writer) { - switch elem := element.(type) { - case *textElement: - buf.Write(elem.text) - case *varElement: - defer func() { - if r := recover(); r != nil { - fmt.Printf("Panic while looking up %q: %s\n", elem.name, r) - } - }() - val := lookup(contextChain, elem.name) - - if val.IsValid() { - if elem.raw { - fmt.Fprint(buf, val.Interface()) - } else { - s := fmt.Sprint(val.Interface()) - template.HTMLEscape(buf, []byte(s)) - } - } - case *sectionElement: - renderSection(elem, contextChain, buf) - case *Template: - elem.renderTemplate(contextChain, buf) - } -} - -func (tmpl *Template) renderTemplate(contextChain []interface{}, buf io.Writer) { - for _, elem := range tmpl.elems { - renderElement(elem, contextChain, buf) - } -} - -func (tmpl *Template) Render(context ...interface{}) string { - var buf bytes.Buffer - var contextChain []interface{} - for _, c := range context { - val := reflect.ValueOf(c) - contextChain = append(contextChain, val) - } - tmpl.renderTemplate(contextChain, &buf) - return buf.String() -} - -func (tmpl *Template) RenderInLayout(layout *Template, context ...interface{}) string { - content := tmpl.Render(context...) - allContext := make([]interface{}, len(context)+1) - copy(allContext[1:], context) - allContext[0] = map[string]string{"content": content} - return layout.Render(allContext...) + for v.IsValid() { + switch av := v; av.Kind() { + case reflect.Ptr: + v = av.Elem() + case reflect.Interface: + v = av.Elem() + default: + break loop + } + } + return v +} + +func (tmpl *Template) renderSection(section *sectionElement, contextChain []interface{}, buf io.Writer) error { + value, err := lookup(contextChain, section.name, true) + if err != nil { + return err + } + var context = contextChain[0].(reflect.Value) + var contexts = []interface{}{} + // if the value is nil, check if it's an inverted section + isEmpty := isEmpty(value) + if isEmpty && !section.inverted || !isEmpty && section.inverted { + return nil + } else if !section.inverted { + valueInd := indirect(value) + switch val := valueInd; val.Kind() { + case reflect.Slice: + for i := 0; i < val.Len(); i++ { + contexts = append(contexts, val.Index(i)) + } + case reflect.Array: + for i := 0; i < val.Len(); i++ { + contexts = append(contexts, val.Index(i)) + } + case reflect.Map, reflect.Struct: + contexts = append(contexts, value) + case reflect.Func: + if val.Type().NumIn() != 2 || val.Type().NumOut() != 2 { + return fmt.Errorf("lambda %q doesn't match required LambaFunc signature", section.name) + } + var text bytes.Buffer + if err := getSectionText(section.elems, &text); err != nil { + return err + } + render := func(text string) (string, error) { + tmpl, err := ParseString(text) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := tmpl.renderTemplate(contextChain, &buf); err != nil { + return "", err + } + return buf.String(), nil + } + in := []reflect.Value{reflect.ValueOf(text.String()), reflect.ValueOf(render)} + res := val.Call(in) + if !res[1].IsNil() { + return fmt.Errorf("lambda %q: %w", section.name, res[1].Interface().(error)) + } + fmt.Fprint(buf, res[0].String()) + return nil + default: + // Spec: Non-false sections have their value at the top of context, + // accessible as {{.}} or through the parent context. This gives + // a simple way to display content conditionally if a variable exists. + contexts = append(contexts, value) + } + } else if section.inverted { + contexts = append(contexts, context) + } + + chain2 := make([]interface{}, len(contextChain)+1) + copy(chain2[1:], contextChain) + //by default we execute the section + for _, ctx := range contexts { + chain2[0] = ctx + for _, elem := range section.elems { + if err := tmpl.renderElement(elem, chain2, buf); err != nil { + return err + } + } + } + return nil +} + +func getSectionText(elements []interface{}, buf io.Writer) error { + for _, element := range elements { + if err := getElementText(element, buf); err != nil { + return err + } + } + return nil +} + +func getElementText(element interface{}, buf io.Writer) error { + switch elem := element.(type) { + case *textElement: + fmt.Fprintf(buf, "%s", elem.text) + case *varElement: + if elem.raw { + fmt.Fprintf(buf, "{{{%s}}}", elem.name) + } else { + fmt.Fprintf(buf, "{{%s}}", elem.name) + } + case *sectionElement: + if elem.inverted { + fmt.Fprintf(buf, "{{^%s}}", elem.name) + } else { + fmt.Fprintf(buf, "{{#%s}}", elem.name) + } + for _, nelem := range elem.elems { + if err := getElementText(nelem, buf); err != nil { + return err + } + } + fmt.Fprintf(buf, "{{/%s}}", elem.name) + default: + return fmt.Errorf("unexpected element type %T", elem) + } + return nil +} + +func (tmpl *Template) renderElement(element interface{}, contextChain []interface{}, buf io.Writer) error { + switch elem := element.(type) { + case *textElement: + _, err := buf.Write(elem.text) + return err + case *varElement: + defer func() { + if r := recover(); r != nil { + fmt.Printf("Panic while looking up %q: %s\n", elem.name, r) + } + }() + val, err := lookup(contextChain, elem.name, AllowMissingVariables) + if err != nil { + return err + } + + if val.IsValid() { + if elem.raw { + fmt.Fprint(buf, val.Interface()) + } else { + s := fmt.Sprint(val.Interface()) + _, _ = buf.Write([]byte(tmpl.escape(s))) + } + } + case *sectionElement: + if err := tmpl.renderSection(elem, contextChain, buf); err != nil { + return err + } + case *partialElement: + partial, err := getPartials(elem.prov, elem.name, elem.indent) + if err != nil { + return err + } + if err := partial.renderTemplate(contextChain, buf); err != nil { + return err + } + } + return nil +} + +func (tmpl *Template) renderTemplate(contextChain []interface{}, buf io.Writer) error { + for _, elem := range tmpl.elems { + if err := tmpl.renderElement(elem, contextChain, buf); err != nil { + return err + } + } + return nil +} + +// FRender uses the given data source - generally a map or struct - to +// render the compiled template to an io.Writer. +func (tmpl *Template) FRender(out io.Writer, context ...interface{}) error { + var contextChain []interface{} + for _, c := range context { + val := reflect.ValueOf(c) + contextChain = append(contextChain, val) + } + return tmpl.renderTemplate(contextChain, out) +} + +// Render uses the given data source - generally a map or struct - to render +// the compiled template and return the output. +func (tmpl *Template) Render(context ...interface{}) (string, error) { + var buf bytes.Buffer + err := tmpl.FRender(&buf, context...) + return buf.String(), err +} + +// RenderInLayout uses the given data source - generally a map or struct - to +// render the compiled template and layout "wrapper" template and return the +// output. +func (tmpl *Template) RenderInLayout(layout *Template, context ...interface{}) (string, error) { + var buf bytes.Buffer + err := tmpl.FRenderInLayout(&buf, layout, context...) + if err != nil { + return "", err + } + return buf.String(), nil +} + +// FRenderInLayout uses the given data source - generally a map or +// struct - to render the compiled templated a loayout "wrapper" +// template to an io.Writer. +func (tmpl *Template) FRenderInLayout(out io.Writer, layout *Template, context ...interface{}) error { + content, err := tmpl.Render(context...) + if err != nil { + return err + } + allContext := make([]interface{}, len(context)+1) + copy(allContext[1:], context) + allContext[0] = map[string]string{"content": content} + return layout.FRender(out, allContext...) } +// ParseString compiles a mustache template string. The resulting output can +// be used to efficiently render the template multiple times with different data +// sources. func ParseString(data string) (*Template, error) { - cwd := os.Getenv("CWD") - tmpl := Template{data, "{{", "}}", 0, 1, cwd, []interface{}{}} - err := tmpl.parse() + return ParseStringRaw(data, false) +} + +// ParseStringRaw compiles a mustache template string. The resulting output can +// be used to efficiently render the template multiple times with different data +// sources. +func ParseStringRaw(data string, forceRaw bool) (*Template, error) { + cwd := os.Getenv("CWD") + partials := &FileProvider{ + Paths: []string{cwd, " "}, + } - if err != nil { - return nil, err - } + return ParseStringPartialsRaw(data, partials, forceRaw) +} - return &tmpl, err +// ParseStringPartials compiles a mustache template string, retrieving any +// required partials from the given provider. The resulting output can be used +// to efficiently render the template multiple times with different data +// sources. +func ParseStringPartials(data string, partials PartialProvider) (*Template, error) { + return ParseStringPartialsRaw(data, partials, false) } +// ParseStringPartialsRaw compiles a mustache template string, retrieving any +// required partials from the given provider. The resulting output can be used +// to efficiently render the template multiple times with different data +// sources. +func ParseStringPartialsRaw(data string, partials PartialProvider, forceRaw bool) (*Template, error) { + tmpl := Template{data, "{{", "}}", 0, 1, []interface{}{}, forceRaw, partials, template.HTMLEscapeString} + err := tmpl.parse() + + if err != nil { + return nil, err + } + + return &tmpl, err +} + +// ParseFile loads a mustache template string from a file and compiles it. The +// resulting output can be used to efficiently render the template multiple +// times with different data sources. func ParseFile(filename string) (*Template, error) { - data, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - dirname, _ := path.Split(filename) - - tmpl := Template{string(data), "{{", "}}", 0, 1, dirname, []interface{}{}} - err = tmpl.parse() - - if err != nil { - return nil, err - } - - return &tmpl, nil -} - -func Render(data string, context ...interface{}) string { - tmpl, err := ParseString(data) - if err != nil { - return err.Error() - } - return tmpl.Render(context...) -} - -func RenderInLayout(data string, layoutData string, context ...interface{}) string { - layoutTmpl, err := ParseString(layoutData) - if err != nil { - return err.Error() - } - tmpl, err := ParseString(data) - if err != nil { - return err.Error() - } - return tmpl.RenderInLayout(layoutTmpl, context...) -} - -func RenderFile(filename string, context ...interface{}) string { - tmpl, err := ParseFile(filename) - if err != nil { - return err.Error() - } - return tmpl.Render(context...) -} - -func RenderFileInLayout(filename string, layoutFile string, context ...interface{}) string { - layoutTmpl, err := ParseFile(layoutFile) - if err != nil { - return err.Error() - } - - tmpl, err := ParseFile(filename) - if err != nil { - return err.Error() - } - return tmpl.RenderInLayout(layoutTmpl, context...) + dirname, _ := path.Split(filename) + partials := &FileProvider{ + Paths: []string{dirname, " "}, + } + + return ParseFilePartials(filename, partials) +} + +// ParseFilePartials loads a mustache template string from a file, retrieving any +// required partials from the given provider, and compiles it. The resulting +// output can be used to efficiently render the template multiple times with +// different data sources. +func ParseFilePartials(filename string, partials PartialProvider) (*Template, error) { + return ParseFilePartialsRaw(filename, false, partials) +} + +// ParseFilePartialsRaw loads a mustache template string from a file, retrieving +// any required partials from the given provider, and compiles it. The resulting +// output can be used to efficiently render the template multiple times with +// different data sources. +func ParseFilePartialsRaw(filename string, forceRaw bool, partials PartialProvider) (*Template, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + tmpl := Template{string(data), "{{", "}}", 0, 1, []interface{}{}, forceRaw, partials, template.HTMLEscapeString} + err = tmpl.parse() + + if err != nil { + return nil, err + } + + return &tmpl, nil +} + +// Render compiles a mustache template string and uses the the given data source +// - generally a map or struct - to render the template and return the output. +func Render(data string, context ...interface{}) (string, error) { + return RenderRaw(data, false, context...) +} + +// RenderRaw compiles a mustache template string and uses the the given data +// source - generally a map or struct - to render the template and return the +// output. +func RenderRaw(data string, forceRaw bool, context ...interface{}) (string, error) { + return RenderPartialsRaw(data, nil, forceRaw, context...) +} + +// RenderPartials compiles a mustache template string and uses the the given partial +// provider and data source - generally a map or struct - to render the template +// and return the output. +func RenderPartials(data string, partials PartialProvider, context ...interface{}) (string, error) { + return RenderPartialsRaw(data, partials, false, context...) +} + +// RenderPartialsRaw compiles a mustache template string and uses the the given +// partial provider and data source - generally a map or struct - to render the +// template and return the output. +func RenderPartialsRaw(data string, partials PartialProvider, forceRaw bool, context ...interface{}) (string, error) { + var tmpl *Template + var err error + if partials == nil { + tmpl, err = ParseStringRaw(data, forceRaw) + } else { + tmpl, err = ParseStringPartialsRaw(data, partials, forceRaw) + } + if err != nil { + return "", err + } + return tmpl.Render(context...) +} + +// RenderInLayout compiles a mustache template string and layout "wrapper" and +// uses the given data source - generally a map or struct - to render the +// compiled templates and return the output. +func RenderInLayout(data string, layoutData string, context ...interface{}) (string, error) { + return RenderInLayoutPartials(data, layoutData, nil, context...) +} + +// RenderInLayoutPartials compiles a mustache template string and layout +// "wrapper" and uses the given data source - generally a map or struct - to +// render the compiled templates and return the output. +func RenderInLayoutPartials(data string, layoutData string, partials PartialProvider, context ...interface{}) (string, error) { + var layoutTmpl, tmpl *Template + var err error + if partials == nil { + layoutTmpl, err = ParseString(layoutData) + } else { + layoutTmpl, err = ParseStringPartials(layoutData, partials) + } + if err != nil { + return "", err + } + + if partials == nil { + tmpl, err = ParseString(data) + } else { + tmpl, err = ParseStringPartials(data, partials) + } + + if err != nil { + return "", err + } + + return tmpl.RenderInLayout(layoutTmpl, context...) +} + +// RenderFile loads a mustache template string from a file and compiles it, and +// then uses the given data source - generally a map or struct - to render the +// template and return the output. +func RenderFile(filename string, context ...interface{}) (string, error) { + tmpl, err := ParseFile(filename) + if err != nil { + return "", err + } + return tmpl.Render(context...) +} + +// RenderFileInLayout loads a mustache template string and layout "wrapper" +// template string from files and compiles them, and then uses the the given +// data source - generally a map or struct - to render the compiled templates +// and return the output. +func RenderFileInLayout(filename string, layoutFile string, context ...interface{}) (string, error) { + layoutTmpl, err := ParseFile(layoutFile) + if err != nil { + return "", err + } + + tmpl, err := ParseFile(filename) + if err != nil { + return "", err + } + return tmpl.RenderInLayout(layoutTmpl, context...) } diff --git a/mustache_test.go b/mustache_test.go index 7019c14..8f92320 100644 --- a/mustache_test.go +++ b/mustache_test.go @@ -1,263 +1,738 @@ package mustache import ( - "os" - "path" - "strings" - "testing" + "bytes" + "errors" + "fmt" + "os" + "path" + "strings" + "testing" ) type Test struct { - tmpl string - context interface{} - expected string + tmpl string + context interface{} + expected string + err error } type Data struct { - A bool - B string + A bool + B string } type User struct { - Name string - Id int64 + Name string + ID int64 } -type settings struct { - Allow bool +type Settings struct { + Allow bool } func (u User) Func1() string { - return u.Name + return u.Name } func (u *User) Func2() string { - return u.Name + return u.Name } func (u *User) Func3() (map[string]string, error) { - return map[string]string{"name": u.Name}, nil + return map[string]string{"name": u.Name}, nil } func (u *User) Func4() (map[string]string, error) { - return nil, nil + return nil, nil } -func (u *User) Func5() (*settings, error) { - return &settings{true}, nil +func (u *User) Func5() (*Settings, error) { + return &Settings{true}, nil } func (u *User) Func6() ([]interface{}, error) { - var v []interface{} - v = append(v, &settings{true}) - return v, nil + var v []interface{} + v = append(v, &Settings{true}) + return v, nil } func (u User) Truefunc1() bool { - return true + return true } func (u *User) Truefunc2() bool { - return true + return true } func makeVector(n int) []interface{} { - var v []interface{} - for i := 0; i < n; i++ { - v = append(v, &User{"Mike", 1}) - } - return v + var v []interface{} + for i := 0; i < n; i++ { + v = append(v, &User{"Mike", 1}) + } + return v } type Category struct { - Tag string - Description string + Tag string + Description string } func (c Category) DisplayName() string { - return c.Tag + " - " + c.Description + return c.Tag + " - " + c.Description } var tests = []Test{ - {`hello world`, nil, "hello world"}, - {`hello {{name}}`, map[string]string{"name": "world"}, "hello world"}, - {`{{var}}`, map[string]string{"var": "5 > 2"}, "5 > 2"}, - {`{{{var}}}`, map[string]string{"var": "5 > 2"}, "5 > 2"}, - {`{{a}}{{b}}{{c}}{{d}}`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "abcd"}, - {`0{{a}}1{{b}}23{{c}}456{{d}}89`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "0a1b23c456d89"}, - {`hello {{! comment }}world`, map[string]string{}, "hello world"}, - {`{{ a }}{{=<% %>=}}<%b %><%={{ }}=%>{{ c }}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc"}, - {`{{ a }}{{= <% %> =}}<%b %><%= {{ }}=%>{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc"}, - - //does not exist - {`{{dne}}`, map[string]string{"name": "world"}, ""}, - {`{{dne}}`, User{"Mike", 1}, ""}, - {`{{dne}}`, &User{"Mike", 1}, ""}, - {`{{#has}}{{/has}}`, &User{"Mike", 1}, ""}, - - //section tests - {`{{#A}}{{B}}{{/A}}`, Data{true, "hello"}, "hello"}, - {`{{#A}}{{{B}}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2"}, - {`{{#A}}{{B}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2"}, - {`{{#A}}{{B}}{{/A}}`, Data{false, "hello"}, ""}, - {`{{a}}{{#b}}{{b}}{{/b}}{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc"}, - {`{{#A}}{{B}}{{/A}}`, struct { - A []struct { - B string - } - }{[]struct { - B string - }{{"a"}, {"b"}, {"c"}}}, - "abc", - }, - {`{{#A}}{{b}}{{/A}}`, struct{ A []map[string]string }{[]map[string]string{{"b": "a"}, {"b": "b"}, {"b": "c"}}}, "abc"}, - - {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike"}, - - {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": nil}, ""}, - {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": (*User)(nil)}, ""}, - {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": []User{}}, ""}, - - {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike"}, - {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []interface{}{&User{"Mike", 12}}}, "Mike"}, - {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": makeVector(1)}, "Mike"}, - {`{{Name}}`, User{"Mike", 1}, "Mike"}, - {`{{Name}}`, &User{"Mike", 1}, "Mike"}, - {"{{#users}}\n{{Name}}\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\nMike\n"}, - {"{{#users}}\r\n{{Name}}\r\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\r\nMike\r\n"}, - - // implicit iterator tests - {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []string{"a", "b", "c", "d", "e"}}, "\"(a)(b)(c)(d)(e)\""}, - {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []int{1, 2, 3, 4, 5}}, "\"(1)(2)(3)(4)(5)\""}, - {`"{{#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)\""}, - - //inverted section tests - {`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]string{"a": "a", "c": "c"}, "abc"}, - {`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]interface{}{"a": "a", "b": false, "c": "c"}, "abc"}, - {`{{^a}}b{{/a}}`, map[string]interface{}{"a": false}, "b"}, - {`{{^a}}b{{/a}}`, map[string]interface{}{"a": true}, ""}, - {`{{^a}}b{{/a}}`, map[string]interface{}{"a": "nonempty string"}, ""}, - {`{{^a}}b{{/a}}`, map[string]interface{}{"a": []string{}}, "b"}, - - //function tests - {`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike"}, - {`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike"}, - {`{{#users}}{{Func2}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike"}, - - {`{{#users}}{{#Func3}}{{name}}{{/Func3}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike"}, - {`{{#users}}{{#Func4}}{{name}}{{/Func4}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, ""}, - {`{{#Truefunc1}}abcd{{/Truefunc1}}`, User{"Mike", 1}, "abcd"}, - {`{{#Truefunc1}}abcd{{/Truefunc1}}`, &User{"Mike", 1}, "abcd"}, - {`{{#Truefunc2}}abcd{{/Truefunc2}}`, &User{"Mike", 1}, "abcd"}, - {`{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}`, &User{"Mike", 1}, "abcd"}, - {`{{#user}}{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd"}, - {`{{#user}}{{#Func6}}{{#Allow}}abcd{{/Allow}}{{/Func6}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd"}, - - //context chaining - {`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"section": map[string]string{"name": "world"}}, "hello world"}, - {`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"name": "bob", "section": map[string]string{"name": "world"}}, "hello world"}, - {`hello {{#bool}}{{#section}}{{name}}{{/section}}{{/bool}}`, map[string]interface{}{"bool": true, "section": map[string]string{"name": "world"}}, "hello world"}, - {`{{#users}}{{canvas}}{{/users}}`, map[string]interface{}{"canvas": "hello", "users": []User{{"Mike", 1}}}, "hello"}, - {`{{#categories}}{{DisplayName}}{{/categories}}`, map[string][]*Category{ - "categories": {&Category{"a", "b"}}, - }, "a - b"}, - - //invalid syntax - https://github.com/hoisie/mustache/issues/10 - {`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "line 1: interleaved closing tag: a"}, - - //dotted names(dot notation) - {`"{{person.name}}" == "{{#person}}{{name}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`}, - {`"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`}, - {`"{{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"`}, - {`"{{a.b.c}}" == ""`, map[string]interface{}{}, `"" == ""`}, - {`"{{a.b.c.name}}" == ""`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "c": map[string]string{"name": "Jim"}}, `"" == ""`}, - {`"{{#a}}{{b.c.d.e.name}}{{/a}}" == "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"}}}}}, "b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Wrong"}}}}}, `"Phil" == "Phil"`}, - {`{{#a}}{{b.c}}{{/a}}`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "b": map[string]string{"c": "ERROR"}}, ""}, + {`hello world`, nil, "hello world", nil}, + {`hello {{name}}`, map[string]string{"name": "world"}, "hello world", nil}, + {`{{var}}`, map[string]string{"var": "5 > 2"}, "5 > 2", nil}, + {`{{{var}}}`, map[string]string{"var": "5 > 2"}, "5 > 2", nil}, + {`{{var}}`, map[string]string{"var": "& \" < >"}, "& " < >", nil}, + {`{{{var}}}`, map[string]string{"var": "& \" < >"}, "& \" < >", nil}, + {`{{a}}{{b}}{{c}}{{d}}`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "abcd", nil}, + {`0{{a}}1{{b}}23{{c}}456{{d}}89`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "0a1b23c456d89", nil}, + {`hello {{! comment }}world`, map[string]string{}, "hello world", nil}, + {`{{ 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 + {`{{#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 > 2", nil}, + {`{{#A}}{{B}}{{/A}}`, Data{false, "hello"}, "", nil}, + {`{{a}}{{#b}}{{b}}{{/b}}{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil}, + {`{{#A}}{{B}}{{/A}}`, struct { + A []struct { + B string + } + }{[]struct { + B string + }{{"a"}, {"b"}, {"c"}}}, + "abc", + nil, + }, + {`{{#A}}{{b}}{{/A}}`, struct{ A []map[string]string }{[]map[string]string{{"b": "a"}, {"b": "b"}, {"b": "c"}}}, "abc", nil}, + + {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil}, + + {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": nil}, "", nil}, + {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": (*User)(nil)}, "", nil}, + {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": []User{}}, "", nil}, + + {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, + {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []interface{}{&User{"Mike", 12}}}, "Mike", nil}, + {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": makeVector(1)}, "Mike", nil}, + {`{{Name}}`, User{"Mike", 1}, "Mike", nil}, + {`{{Name}}`, &User{"Mike", 1}, "Mike", nil}, + {"{{#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 + {"{{#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}, + {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0.0}, "", nil}, + {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": ""}, "", nil}, + {"{{#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 + {"{{#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 + {`{{#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 + {`{{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 + {`{{#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}, + + {`{{#users}}{{#Func3}}{{name}}{{/Func3}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, + {`{{#users}}{{#Func4}}{{name}}{{/Func4}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "", nil}, + {`{{#Truefunc1}}abcd{{/Truefunc1}}`, User{"Mike", 1}, "abcd", nil}, + {`{{#Truefunc1}}abcd{{/Truefunc1}}`, &User{"Mike", 1}, "abcd", nil}, + {`{{#Truefunc2}}abcd{{/Truefunc2}}`, &User{"Mike", 1}, "abcd", nil}, + {`{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}`, &User{"Mike", 1}, "abcd", nil}, + {`{{#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 + {`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}, + {`{{#users}}{{canvas}}{{/users}}`, map[string]interface{}{"canvas": "hello", "users": []User{{"Mike", 1}}}, "hello", nil}, + {`{{#categories}}{{DisplayName}}{{/categories}}`, map[string][]*Category{ + "categories": {&Category{"a", "b"}}, + }, "a - b", nil}, + + {`{{#section}}{{#bool}}{{x}}{{/bool}}{{/section}}`, + map[string]interface{}{ + "x": "broken", + "section": []map[string]interface{}{ + {"x": "working", "bool": true}, + {"x": "nope", "bool": false}, + }, + }, "working", nil}, + + {`{{#section}}{{^bool}}{{x}}{{/bool}}{{/section}}`, + map[string]interface{}{ + "x": "broken", + "section": []map[string]interface{}{ + {"x": "working", "bool": false}, + {"x": "nope", "bool": true}, + }, + }, "working", nil}, + + //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}, + {`"{{#a}}{{b.c.d.e.name}}{{/a}}" == "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"}}}}}, "b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Wrong"}}}}}, `"Phil" == "Phil"`, nil}, } func TestBasic(t *testing.T) { - for _, test := range tests { - output := Render(test.tmpl, test.context) - if output != test.expected { - t.Fatalf("%q expected %q got %q", test.tmpl, test.expected, output) - } - } + // Default behavior, AllowMissingVariables=true + for _, test := range tests { + output, err := Render(test.tmpl, test.context) + if err != nil { + t.Errorf("%q expected %q but got error %q", test.tmpl, test.expected, err.Error()) + } else if output != test.expected { + t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) + } + } + + // Now set AllowMissingVariables=false and test again + AllowMissingVariables = false + defer func() { AllowMissingVariables = true }() + for _, test := range tests { + output, err := Render(test.tmpl, test.context) + if err != nil { + t.Errorf("%s expected %s but got error %s", test.tmpl, test.expected, err.Error()) + } else if output != test.expected { + t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) + } + } +} + +var missing = []Test{ + //does not exist + {`{{dne}}`, map[string]string{"name": "world"}, "", nil}, + {`{{dne}}`, User{"Mike", 1}, "", nil}, + {`{{dne}}`, &User{"Mike", 1}, "", nil}, + //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}, +} + +func TestMissing(t *testing.T) { + // Default behavior, AllowMissingVariables=true + for _, test := range missing { + output, err := Render(test.tmpl, test.context) + if err != nil { + t.Error(err) + } else if output != test.expected { + t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) + } + } + + // Now set AllowMissingVariables=false and confirm we get errors. + AllowMissingVariables = false + defer func() { AllowMissingVariables = true }() + for _, test := range missing { + output, err := Render(test.tmpl, test.context) + if err == nil { + t.Errorf("%q expected missing variable error but got %q", test.tmpl, output) + } else if !strings.Contains(err.Error(), "missing variable") { + t.Errorf("%q expected missing variable error but got %q", test.tmpl, err.Error()) + } + } } func TestFile(t *testing.T) { - filename := path.Join(path.Join(os.Getenv("PWD"), "tests"), "test1.mustache") - expected := "hello world" - output := RenderFile(filename, map[string]string{"name": "world"}) - if output != expected { - t.Fatalf("testfile expected %q got %q", expected, output) - } + filename := path.Join(path.Join(os.Getenv("PWD"), "tests"), "test1.mustache") + expected := "hello world" + output, err := RenderFile(filename, map[string]string{"name": "world"}) + if err != nil { + t.Error(err) + } else if output != expected { + t.Errorf("testfile expected %q got %q", expected, output) + } +} + +func TestFRender(t *testing.T) { + filename := path.Join(path.Join(os.Getenv("PWD"), "tests"), "test1.mustache") + expected := "hello world" + tmpl, err := ParseFile(filename) + if err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + err = tmpl.FRender(&buf, map[string]string{"name": "world"}) + if err != nil { + t.Fatal(err) + } + output := buf.String() + if output != expected { + t.Fatalf("testfile expected %q got %q", expected, output) + } } func TestPartial(t *testing.T) { - filename := path.Join(path.Join(os.Getenv("PWD"), "tests"), "test2.mustache") - println(filename) - expected := "hello world" - output := RenderFile(filename, map[string]string{"Name": "world"}) - if output != expected { - t.Fatalf("testpartial expected %q got %q", expected, output) - } + filename := path.Join(path.Join(os.Getenv("PWD"), "tests"), "test2.mustache") + expected := "hello world" + tmpl, err := ParseFile(filename) + if err != nil { + t.Error(err) + return + } + output, err := tmpl.Render(map[string]string{"Name": "world"}) + if err != nil { + t.Error(err) + return + } else if output != expected { + t.Errorf("testpartial expected %q got %q", expected, output) + return + } + + expectedTags := []tag{ + { + Type: Partial, + Name: "partial", + }, + } + compareTags(t, tmpl.Tags(), expectedTags) } /* -func TestSectionPartial(t *testing.T) { - filename := path.Join(path.Join(os.Getenv("PWD"), "tests"), "test3.mustache") - expected := "Mike\nJoe\n" - context := map[string]interface{}{"users": []User{{"Mike", 1}, {"Joe", 2}}} - output := RenderFile(filename, context) - if output != expected { - t.Fatalf("testSectionPartial expected %q got %q", expected, output) - } -} + func TestSectionPartial(t *testing.T) { + filename := path.Join(path.Join(os.Getenv("PWD"), "tests"), "test3.mustache") + expected := "Mike\nJoe\n" + context := map[string]interface{}{"users": []User{{"Mike", 1}, {"Joe", 2}}} + output := RenderFile(filename, context) + if output != expected { + t.Fatalf("testSectionPartial expected %q got %q", expected, output) + } + } */ func TestMultiContext(t *testing.T) { - output := Render(`{{hello}} {{World}}`, map[string]string{"hello": "hello"}, struct{ World string }{"world"}) - output2 := Render(`{{hello}} {{World}}`, struct{ World string }{"world"}, map[string]string{"hello": "hello"}) - if output != "hello world" || output2 != "hello world" { - t.Fatalf("TestMultiContext expected %q got %q", "hello world", output) - } + output, err := Render(`{{hello}} {{World}}`, map[string]string{"hello": "hello"}, struct{ World string }{"world"}) + if err != nil { + t.Error(err) + return + } + output2, err := Render(`{{hello}} {{World}}`, struct{ World string }{"world"}, map[string]string{"hello": "hello"}) + if err != nil { + t.Error(err) + return + } + if output != "hello world" || output2 != "hello world" { + t.Errorf("TestMultiContext expected %q got %q", "hello world", output) + return + } +} + +func TestLambda(t *testing.T) { + tmpl := `{{#lambda}}Hello {{name}}. {{#sub}}{{.}} {{/sub}}{{^negsub}}nothing{{/negsub}}{{/lambda}}` + data := map[string]interface{}{ + "name": "world", + "sub": []string{"subv1", "subv2"}, + "lambda": func(text string, render RenderFunc) (string, error) { + res, err := render(text) + return res + "!", err + }, + } + + output, err := Render(tmpl, data) + if err != nil { + t.Fatal(err) + } + expect := "Hello world. subv1 subv2 nothing!" + if output != expect { + t.Fatalf("TestLambda expected %q got %q", expect, output) + } +} + +func TestLambdaStruct(t *testing.T) { + tmpl := `{{#Lambda}}Hello {{Name}}. {{#Sub}}{{.}} {{/Sub}}{{^Negsub}}nothing{{/Negsub}}{{/Lambda}}` + data := struct { + Name string + Sub []string + Lambda LambdaFunc + }{ + Name: "world", + Sub: []string{"subv1", "subv2"}, + Lambda: func(text string, render RenderFunc) (string, error) { + res, err := render(text) + return res + "!", err + }, + } + + output, err := Render(tmpl, data) + if err != nil { + t.Fatal(err) + } + expect := "Hello world. subv1 subv2 nothing!" + if output != expect { + t.Fatalf("TestLambdaStruct expected %q got %q", expect, output) + } +} + +func TestLambdaRawTag(t *testing.T) { + tmpl := `{{#Lambda}}Hello {{{Name}}}.{{/Lambda}}` + data := struct { + Name string + Lambda LambdaFunc + }{ + Name: "
", + Lambda: func(text string, render RenderFunc) (string, error) { + return render(text) + }, + } + + output, err := Render(tmpl, data) + if err != nil { + t.Fatal(err) + } + expect := "Hello
." + if output != expect { + t.Fatalf("TestLambdaStruct expected %q got %q", expect, output) + } +} + +func TestLambdaError(t *testing.T) { + tmpl := `{{#lambda}}{{/lambda}}` + data := map[string]interface{}{ + "lambda": func(text string, render RenderFunc) (string, error) { + return "", fmt.Errorf("test err") + }, + } + _, err := Render(tmpl, data) + if err == nil { + t.Fatal("nil error") + } + + expect := `lambda "lambda": test err` + if err.Error() != expect { + t.Fatalf("TestLambdaError expected %q got %q", expect, err.Error()) + } +} + +func TestLambdaWrongSignature(t *testing.T) { + tmpl := `{{#lambda}}{{/lambda}}` + data := map[string]interface{}{ + "lambda": func(text string, render RenderFunc, _ string) (string, error) { + return render(text) + }, + } + _, err := Render(tmpl, data) + if err == nil { + t.Fatal("nil error") + } + + expect := `lambda "lambda" doesn't match required LambaFunc signature` + if err.Error() != expect { + t.Fatalf("TestLambdaWrongSignature expected %q got %q", expect, err.Error()) + } } var malformed = []Test{ - {`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "empty tag"}, - {`{{}}`, nil, "empty tag"}, - {`{{}`, nil, "unmatched open tag"}, - {`{{`, nil, "unmatched open tag"}, + {`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")}, + {`{{}}`, 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 + {`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")}, } func TestMalformed(t *testing.T) { - for _, test := range malformed { - output := Render(test.tmpl, test.context) - if strings.Index(output, test.expected) == -1 { - t.Fatalf("%q expected %q in error %q", test.tmpl, test.expected, output) - } - } + for _, test := range malformed { + output, err := Render(test.tmpl, test.context) + if err != nil { + if test.err == nil { + t.Error(err) + } else if test.err.Error() != err.Error() { + t.Errorf("%q expected error %q but got error %q", test.tmpl, test.err.Error(), err.Error()) + } + } else { + if test.err == nil { + t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) + } else { + t.Errorf("%q expected error %q but got %q", test.tmpl, test.err.Error(), output) + } + } + } +} + +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 - context interface{} - expected string + layout string + tmpl string + context interface{} + expected string } var layoutTests = []LayoutTest{ - {`Header {{content}} Footer`, `Hello World`, nil, `Header Hello World Footer`}, - {`Header {{content}} Footer`, `Hello {{s}}`, map[string]string{"s": "World"}, `Header Hello World Footer`}, - {`Header {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World"}, `Header Hello World Footer`}, - {`Header {{extra}} {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World", "extra": "extra"}, `Header extra Hello World Footer`}, - {`Header {{content}} {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World"}, `Header Hello World Hello World Footer`}, + {`Header {{content}} Footer`, `Hello World`, nil, `Header Hello World Footer`}, + {`Header {{content}} Footer`, `Hello {{s}}`, map[string]string{"s": "World"}, `Header Hello World Footer`}, + {`Header {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World"}, `Header Hello World Footer`}, + {`Header {{extra}} {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World", "extra": "extra"}, `Header extra Hello World Footer`}, + {`Header {{content}} {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World"}, `Header Hello World Hello World Footer`}, } func TestLayout(t *testing.T) { - for _, test := range layoutTests { - output := RenderInLayout(test.tmpl, test.layout, test.context) - if output != test.expected { - t.Fatalf("%q expected %q got %q", test.tmpl, test.expected, output) - } - } + for _, test := range layoutTests { + output, err := RenderInLayout(test.tmpl, test.layout, test.context) + if err != nil { + t.Error(err) + } else if output != test.expected { + t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) + } + } +} + +func TestLayoutToWriter(t *testing.T) { + for _, test := range layoutTests { + tmpl, err := ParseString(test.tmpl) + if err != nil { + t.Error(err) + continue + } + layoutTmpl, err := ParseString(test.layout) + if err != nil { + t.Error(err) + continue + } + var buf bytes.Buffer + err = tmpl.FRenderInLayout(&buf, layoutTmpl, test.context) + if err != nil { + t.Error(err) + } else if buf.String() != test.expected { + t.Errorf("%q expected %q got %q", test.tmpl, test.expected, buf.String()) + } + } +} + +type Person struct { + FirstName string + LastName string +} + +func (p *Person) Name1() string { + return p.FirstName + " " + p.LastName +} + +func (p Person) Name2() string { + return p.FirstName + " " + p.LastName +} + +func TestPointerReceiver(t *testing.T) { + p := Person{"John", "Smith"} + tests := []struct { + tmpl string + context interface{} + expected string + }{ + { + tmpl: "{{Name1}}", + context: &p, + expected: "John Smith", + }, + { + tmpl: "{{Name2}}", + context: &p, + expected: "John Smith", + }, + { + tmpl: "{{Name1}}", + context: p, + expected: "", + }, + { + tmpl: "{{Name2}}", + context: p, + expected: "John Smith", + }, + } + for _, test := range tests { + output, err := Render(test.tmpl, test.context) + if err != nil { + t.Error(err) + } else if output != test.expected { + t.Errorf("expected %q got %q", test.expected, output) + } + } +} + +type tag struct { + Type TagType + Name string + Tags []tag +} + +type tagsTest struct { + tmpl string + tags []tag +} + +var tagTests = []tagsTest{ + { + tmpl: `hello world`, + tags: nil, + }, + { + tmpl: `hello {{name}}`, + tags: []tag{ + { + Type: Variable, + Name: "name", + }, + }, + }, + { + tmpl: `{{#name}}hello {{name}}{{/name}}{{^name}}hello {{name2}}{{/name}}`, + tags: []tag{ + { + Type: Section, + Name: "name", + Tags: []tag{ + { + Type: Variable, + Name: "name", + }, + }, + }, + { + Type: InvertedSection, + Name: "name", + Tags: []tag{ + { + Type: Variable, + Name: "name2", + }, + }, + }, + }, + }, +} + +func TestTags(t *testing.T) { + for _, test := range tagTests { + testTags(t, &test) + } +} + +func testTags(t *testing.T, test *tagsTest) { + tmpl, err := ParseString(test.tmpl) + if err != nil { + t.Error(err) + return + } + compareTags(t, tmpl.Tags(), test.tags) +} + +func compareTags(t *testing.T, actual []Tag, expected []tag) { + if len(actual) != len(expected) { + t.Errorf("expected %d tags, got %d", len(expected), len(actual)) + return + } + for i, tag := range actual { + if tag.Type() != expected[i].Type { + t.Errorf("expected %s, got %s", expected[i].Type, tag.Type()) + return + } + if tag.Name() != expected[i].Name { + t.Errorf("expected %s, got %s", expected[i].Name, tag.Name()) + return + } + + switch tag.Type() { + case Variable: + if len(expected[i].Tags) != 0 { + t.Errorf("expected %d tags, got 0", len(expected[i].Tags)) + return + } + case Section, InvertedSection: + compareTags(t, tag.Tags(), expected[i].Tags) + case Partial: + compareTags(t, tag.Tags(), expected[i].Tags) + case Invalid: + t.Errorf("invalid tag type: %s", tag.Type()) + return + default: + t.Errorf("invalid tag type: %s", tag.Type()) + return + } + } +} + +func TestCustomEscape(t *testing.T) { + templ, err := ParseString("Hello {{value}}!") + if err != nil { + t.Fatalf("default template should be parsed") + } + templ.Escape(func(text string) string { + return "!" + text + "!" + }) + + value, err := templ.Render(map[string]string{"value": "world"}) + if err != nil { + t.Errorf("expected to be rendered, got %v", err) + } + const expected = "Hello !world!!" + + if value != expected { + t.Errorf("expected %s, got %v", expected, value) + } } diff --git a/partials.go b/partials.go new file mode 100644 index 0000000..99cabe2 --- /dev/null +++ b/partials.go @@ -0,0 +1,101 @@ +package mustache + +import ( + "os" + "path" + "regexp" +) + +// PartialProvider comprises the behaviors required of a struct to be able to provide partials to the mustache rendering +// engine. +type PartialProvider interface { + // Get accepts the name of a partial and returns the parsed partial, if it could be found; a valid but empty + // template, if it could not be found; or nil and error if an error occurred (other than an inability to find + // the partial). + Get(name string) (string, error) +} + +// FileProvider implements the PartialProvider interface by providing partials drawn from a filesystem. When a partial +// named `NAME` is requested, FileProvider searches each listed path for a file named as `NAME` followed by any of the +// listed extensions. The default for `Paths` is to search the current working directory. The default for `Extensions` +// is to examine, in order, no extension; then ".mustache"; then ".stache". +type FileProvider struct { + Paths []string + Extensions []string +} + +// Get accepts the name of a partial and returns the parsed partial. +func (fp *FileProvider) Get(name string) (string, error) { + var filename string + + var paths []string + if fp.Paths != nil { + paths = fp.Paths + } else { + paths = []string{""} + } + + var exts []string + if fp.Extensions != nil { + exts = fp.Extensions + } else { + exts = []string{"", ".mustache", ".stache"} + } + + for _, p := range paths { + for _, e := range exts { + name := path.Join(p, name+e) + f, err := os.Open(name) + if err == nil { + filename = name + f.Close() + break + } + } + } + + if filename == "" { + return "", nil + } + + data, err := os.ReadFile(filename) + if err != nil { + return "", err + } + + return string(data), nil +} + +var _ PartialProvider = (*FileProvider)(nil) + +// StaticProvider implements the PartialProvider interface by providing partials drawn from a map, which maps partial +// name to template contents. +type StaticProvider struct { + Partials map[string]string +} + +// Get accepts the name of a partial and returns the parsed partial. +func (sp *StaticProvider) Get(name string) (string, error) { + if sp.Partials != nil { + if data, ok := sp.Partials[name]; ok { + return data, nil + } + } + + return "", nil +} + +var _ PartialProvider = (*StaticProvider)(nil) + +func getPartials(partials PartialProvider, name, indent string) (*Template, error) { + data, err := partials.Get(name) + if err != nil { + return nil, err + } + + // indent non empty lines + r := regexp.MustCompile(`(?m:^(.+)$)`) + data = r.ReplaceAllString(data, indent+"$1") + + return ParseStringPartials(data, partials) +} diff --git a/spec b/spec new file mode 160000 index 0000000..7b09c52 --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit 7b09c52a149563d53156e7abcff3dde02f7b5b9d diff --git a/spec_test.go b/spec_test.go new file mode 100644 index 0000000..89159e5 --- /dev/null +++ b/spec_test.go @@ -0,0 +1,133 @@ +package mustache + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "testing" +) + +var disabledTests = map[string]map[string]struct{}{ + "interpolation.json": { + // disabled b/c Go uses """ in place of """ + // both are valid escapings, and we validate the behavior in mustache_test.go + "HTML Escaping": struct{}{}, + "Implicit Iterators - HTML Escaping": struct{}{}, + // Not currently compliant with null interpolation tests added in v1.2.1 + "Basic Null Interpolation": struct{}{}, + "Triple Mustache Null Interpolation": struct{}{}, + "Ampersand Null Interpolation": struct{}{}, + }, + "~inheritance.json": {}, // not implemented + "~lambdas.json": { + "Interpolation": struct{}{}, + "Interpolation - Expansion": struct{}{}, + "Interpolation - Alternate Delimiters": struct{}{}, + "Interpolation - Multiple Calls": struct{}{}, + "Escaping": struct{}{}, + "Section - Alternate Delimiters": struct{}{}, + "Inverted Section": struct{}{}, + }, +} + +type specTest struct { + Name string `json:"name"` + Data interface{} `json:"data"` + Expected string `json:"expected"` + Template string `json:"template"` + Description string `json:"desc"` + Partials map[string]string `json:"partials"` +} + +type specTestSuite struct { + Tests []specTest `json:"tests"` +} + +func TestSpec(t *testing.T) { + root := filepath.Join(os.Getenv("PWD"), "spec", "specs") + if _, err := os.Stat(root); err != nil { + if os.IsNotExist(err) { + t.Fatalf("Could not find the specs folder at %s, ensure the submodule exists by running 'git submodule update --init'", root) + } + t.Fatal(err) + } + + paths, err := filepath.Glob(root + "/*.json") + if err != nil { + t.Fatal(err) + } + sort.Strings(paths) + + for _, path := range paths { + _, file := filepath.Split(path) + b, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var suite specTestSuite + err = json.Unmarshal(b, &suite) + if err != nil { + t.Fatal(err) + } + for _, test := range suite.Tests { + runTest(t, file, &test) + } + } +} + +func runTest(t *testing.T, file string, test *specTest) { + disabled, ok := disabledTests[file] + if ok { + // Can disable a single test or the entire file. + if _, ok := disabled[test.Name]; ok || len(disabled) == 0 { + t.Logf("[%s %s]: Skipped", file, test.Name) + return + } + } + + // We can't generate lambda functions at runtime; instead we define them in + // code to match the spec tests and inject them here at runtime. + if file == "~lambdas.json" { + test.Data.(map[string]interface{})["lambda"] = lambdas[test.Name] + } + + var out string + var err error + if len(test.Partials) > 0 { + out, err = RenderPartials(test.Template, &StaticProvider{test.Partials}, test.Data) + } else { + out, err = Render(test.Template, test.Data) + } + if err != nil { + t.Errorf("[%s %s]: %s", file, test.Name, err.Error()) + return + } + if out != test.Expected { + t.Errorf("[%s %s]: Expected %q, got %q", file, test.Name, test.Expected, out) + return + } + + t.Logf("[%s %s]: Passed", file, test.Name) +} + +// Define the lambda functions to match those in the spec tests. The javascript +// implementations from the spec tests are included for reference. +var lambdas = map[string]LambdaFunc{ + "Section": func(text string, render RenderFunc) (string, error) { + // function(txt) { return (txt == "{{x}}" ? "yes" : "no") } + if text == "{{x}}" { + return "yes", nil + } else { + return "no", nil + } + }, + "Section - Expansion": func(text string, render RenderFunc) (string, error) { + // function(txt) { return txt + "{{planet}}" + txt } + return render(text + "{{planet}}" + text) + }, + "Section - Multiple Calls": func(text string, render RenderFunc) (string, error) { + // function(txt) { return "__" + txt + "__" } + return render("__" + text + "__") + }, +}