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)
+
+
+
+
+----
+
+## 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 + "__")
+ },
+}