46 | {{content}}
47 |
48 |
49 |
50 | {{end}}
51 |
52 | {{block header()}}
53 | {{yield row() content}}
54 | {{yield col(md=6,additionalClass="center") content}}
55 | {{yield content}}
56 | {{end}}
57 | {{end}}
58 | {{end}}
59 |
60 | {{include "include.jet"}}
61 |
--------------------------------------------------------------------------------
/loaders/multi/multi.go:
--------------------------------------------------------------------------------
1 | package multi
2 |
3 | import (
4 | "io"
5 | "os"
6 |
7 | "github.com/CloudyKit/jet/v6"
8 | )
9 |
10 | var _ jet.Loader = (*Multi)(nil)
11 |
12 | // Multi implements jet.Loader interface and tries to load templates from a list of custom loaders.
13 | // Caution: When multiple loaders have templates with the same name, the order in which you pass loaders
14 | // to NewLoader/AddLoaders dictates which template will be returned by Open when you request it!
15 | type Multi struct {
16 | loaders []jet.Loader
17 | }
18 |
19 | // NewLoader returns a new multi loader. The order of the loaders passed as parameters
20 | // will define the order in which templates are loaded.
21 | func NewLoader(loaders ...jet.Loader) *Multi {
22 | return &Multi{loaders: loaders}
23 | }
24 |
25 | // AddLoaders adds the passed loaders to the list of loaders.
26 | func (m *Multi) AddLoaders(loaders ...jet.Loader) {
27 | m.loaders = append(m.loaders, loaders...)
28 | }
29 |
30 | // ClearLoaders clears the list of loaders.
31 | func (m *Multi) ClearLoaders() {
32 | m.loaders = nil
33 | }
34 |
35 | // Open will open the file passed by trying all loaders in succession.
36 | func (m *Multi) Open(name string) (io.ReadCloser, error) {
37 | for _, loader := range m.loaders {
38 | if f, err := loader.Open(name); err == nil {
39 | return f, nil
40 | }
41 | }
42 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
43 | }
44 |
45 | // Exists checks all loaders in succession, returning true if the template file was found or false
46 | // if no loader can provide the file.
47 | func (m *Multi) Exists(name string) bool {
48 | for _, loader := range m.loaders {
49 | if ok := loader.Exists(name); ok {
50 | return true
51 | }
52 | }
53 | return false
54 | }
55 |
--------------------------------------------------------------------------------
/ranger_test.go:
--------------------------------------------------------------------------------
1 | package jet
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "reflect"
7 | )
8 |
9 | // exampleCustomBenchRanger satisfies the Ranger interface, generating fixed
10 | // data.
11 | type exampleCustomRanger struct {
12 | i int
13 | }
14 |
15 | // Type assertion to verify exampleCustomRanger satisfies the Ranger interface.
16 | var _ Ranger = (*exampleCustomRanger)(nil)
17 |
18 | func (ecr *exampleCustomRanger) ProvidesIndex() bool {
19 | // Return false if 'k' can't be filled in Range().
20 | return true
21 | }
22 |
23 | func (ecr *exampleCustomRanger) Range() (k reflect.Value, v reflect.Value, done bool) {
24 | if ecr.i >= 3 {
25 | done = true
26 | return
27 | }
28 |
29 | k = reflect.ValueOf(ecr.i)
30 | v = reflect.ValueOf(fmt.Sprintf("custom ranger %d", ecr.i))
31 | ecr.i += 1
32 | return
33 | }
34 |
35 | // ExampleRanger demonstrates how to write a custom template ranger.
36 | func ExampleRanger() {
37 | // Error handling ignored for brevity.
38 | //
39 | // Setup template and rendering.
40 | loader := NewInMemLoader()
41 | loader.Set("template",
42 | `{{ range k := ecr }}
43 | {{k}} = {{.}}
44 | {{- end }}
45 | {{ range k := struct.RangerEface }}
46 | {{k}} = {{.}}
47 | {{- end }}`,
48 | )
49 | set := NewSet(loader, WithSafeWriter(nil))
50 | t, _ := set.GetTemplate("template")
51 |
52 | // Pass a custom ranger instance as the 'ecr' var, as well as in a struct field with type interface{}.
53 | vars := VarMap{
54 | "ecr": reflect.ValueOf(&exampleCustomRanger{}),
55 | "struct": reflect.ValueOf(struct{ RangerEface interface{} }{RangerEface: &exampleCustomRanger{}}),
56 | }
57 |
58 | // Execute template.
59 | _ = t.Execute(os.Stdout, vars, nil)
60 |
61 | // Output:
62 | // 0 = custom ranger 0
63 | // 1 = custom ranger 1
64 | // 2 = custom ranger 2
65 | //
66 | // 0 = custom ranger 0
67 | // 1 = custom ranger 1
68 | // 2 = custom ranger 2
69 | }
70 |
--------------------------------------------------------------------------------
/jettest/test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jettest
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "strings"
21 | "testing"
22 |
23 | "github.com/CloudyKit/jet/v6"
24 | )
25 |
26 | func RunWithSet(t *testing.T, set *jet.Set, variables jet.VarMap, context interface{}, testName, testExpected string) {
27 | tt, err := set.GetTemplate(testName)
28 | if err != nil {
29 | t.Errorf("Error parsing templates for test %s: %v", testName, err)
30 | return
31 | }
32 | RunWithTemplate(t, tt, variables, context, testExpected)
33 | }
34 |
35 | func RunWithTemplate(t *testing.T, tt *jet.Template, variables jet.VarMap, context interface{}, testExpected string) {
36 | if testing.RunTests(func(pat, str string) (bool, error) {
37 | return true, nil
38 | }, []testing.InternalTest{
39 | {
40 | Name: fmt.Sprintf("\tJetTest(%s)", tt.Name),
41 | F: func(t *testing.T) {
42 | var buf bytes.Buffer
43 | err := tt.Execute(&buf, variables, context)
44 | if err != nil {
45 | t.Errorf("Eval error: %q executing %s", err.Error(), tt.Name)
46 | return
47 | }
48 | result := strings.Replace(buf.String(), "\r\n", "\n", -1)
49 | if result != testExpected {
50 | t.Errorf("Result error expected %q got %q on %s", testExpected, result, tt.Name)
51 | }
52 | },
53 | },
54 | }) == false {
55 | t.Fail()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jet Template Engine for Go
2 |
3 | [](https://travis-ci.org/CloudyKit/jet) [](https://ci.appveyor.com/project/CloudyKit/jet) [](https://gitter.im/CloudyKit/jet)
4 |
5 | Jet is a template engine developed to be easy to use, powerful, dynamic, yet secure and very fast.
6 |
7 | * simple and familiar syntax
8 | * supports template inheritance (`extends`) and composition (`block`/`yield`, `import`, `include`)
9 | * descriptive error messages with filename and line number
10 | * auto-escaping
11 | * simple C-like expressions
12 | * very fast execution – Jet can execute templates faster than some pre-compiled template engines
13 | * very light in terms of allocations and memory footprint
14 |
15 | ## v6
16 |
17 | Version 6 brings major improvements to the Go API. Make sure to read through the [breaking changes](./docs/changes.md) before making the jump.
18 |
19 | ## Docs
20 |
21 | - [Go API](https://pkg.go.dev/github.com/CloudyKit/jet/v6#section-documentation)
22 | - [Syntax Reference](./docs/syntax.md)
23 | - [Built-ins](./docs/builtins.md)
24 | - [Wiki](https://github.com/CloudyKit/jet/wiki) (some things are out of date)
25 |
26 | ## Example application
27 |
28 | An example to-do application is available in [examples/todos](./examples/todos). Clone the repository, then (in the repository root) do:
29 | ```
30 | $ cd examples/todos; go run main.go
31 | ```
32 |
33 | ## IntelliJ Plugin
34 |
35 | If you use IntelliJ there is a plugin available at https://github.com/jhsx/GoJetPlugin.
36 | There is also a very good Go plugin for IntelliJ – see https://github.com/go-lang-plugin-org/go-lang-idea-plugin.
37 | GoJetPlugin + Go-lang-idea-plugin = happiness!
38 |
39 | ## Contributing
40 |
41 | All contributions are welcome – if you find a bug please report it.
42 |
43 | ## Contributors
44 |
45 | - José Santos (@jhsx)
46 | - Daniel Lohse (@annismckenzie)
47 | - Alexander Willing (@sauerbraten)
48 |
--------------------------------------------------------------------------------
/exec.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Jet is a fast and dynamic template engine for the Go programming language, set of features
16 | // includes very fast template execution, a dynamic and flexible language, template inheritance, low number of allocations,
17 | // special interfaces to allow even further optimizations.
18 |
19 | package jet
20 |
21 | import (
22 | "io"
23 | "reflect"
24 | "sort"
25 | )
26 |
27 | type VarMap map[string]reflect.Value
28 |
29 | // SortedKeys returns a sorted slice of VarMap keys
30 | func (scope VarMap) SortedKeys() []string {
31 | keys := make([]string, 0, len(scope))
32 | for k := range scope {
33 | keys = append(keys, k)
34 | }
35 | sort.Strings(keys)
36 | return keys
37 | }
38 |
39 | func (scope VarMap) Set(name string, v interface{}) VarMap {
40 | scope[name] = reflect.ValueOf(v)
41 | return scope
42 | }
43 |
44 | func (scope VarMap) SetFunc(name string, v Func) VarMap {
45 | scope[name] = reflect.ValueOf(v)
46 | return scope
47 | }
48 |
49 | func (scope VarMap) SetWriter(name string, v SafeWriter) VarMap {
50 | scope[name] = reflect.ValueOf(v)
51 | return scope
52 | }
53 |
54 | // Execute executes the template into w.
55 | func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) (err error) {
56 | st := pool_State.Get().(*Runtime)
57 | defer st.recover(&err)
58 |
59 | st.blocks = t.processedBlocks
60 | st.variables = variables
61 | st.set = t.set
62 | st.Writer = w
63 |
64 | // resolve extended template
65 | for t.extends != nil {
66 | t = t.extends
67 | }
68 |
69 | if data != nil {
70 | st.context = reflect.ValueOf(data)
71 | }
72 |
73 | st.executeList(t.Root)
74 | return
75 | }
76 |
--------------------------------------------------------------------------------
/testData/devdump.jet:
--------------------------------------------------------------------------------
1 | 1{{- x1:="a string" }}
2 | 2{{- x2 := 1 }}
3 | 3{{- b1 := true }}
4 | 4{{- b2 := false }}
5 | 5{{- s1 := slice("foo", "bar", "baz", "duq")}}
6 | 6{{- m := map("foo", 123) }}
7 | 7{{- mainMenu := "a variable, not a block!" }}
8 | 8{{ block mainMenu(type="text", label="main") }}inside a block{{ end }}
9 | ------------------------------------- dump without parameters
10 | {{ dump() }}
11 | ------------------------------------- dump with depth of 2
12 | {{ dump(2) }}
13 | ------------------------------------- dump with erroneous use
14 | {{ try }} {{ dump(1,"m") }} {{ catch err }} {{- err.Error() -}} {{ end }}
15 | ------------------------------------- dump named
16 | {{ dump("mainMenu", "m") }}
17 | done
18 | ===
19 | 1
20 | 2
21 | 3
22 | 4
23 | 5
24 | 6
25 | 7
26 | 8inside a block
27 | ------------------------------------- dump without parameters
28 | Context:
29 | struct { Name string; Surname string } struct { Name string; Surname string }{Name:"John", Surname:"Doe"}
30 | Variables in current scope:
31 | b1=true
32 | b2=false
33 | m=map[string]interface {}{"foo":123}
34 | mainMenu="a variable, not a block!"
35 | s1=[]interface {}{"foo", "bar", "baz", "duq"}
36 | x1="a string"
37 | x2=1
38 | Blocks:
39 | block mainMenu(type="text",label="main"), from /devdump.jet
40 |
41 | ------------------------------------- dump with depth of 2
42 | Context:
43 | struct { Name string; Surname string } struct { Name string; Surname string }{Name:"John", Surname:"Doe"}
44 | Variables in current scope:
45 | b1=true
46 | b2=false
47 | m=map[string]interface {}{"foo":123}
48 | mainMenu="a variable, not a block!"
49 | s1=[]interface {}{"foo", "bar", "baz", "duq"}
50 | x1="a string"
51 | x2=1
52 | Variables in scope 1 level(s) up:
53 | aSlice=[]string{"sliceMember1", "sliceMember2"}
54 | inputMap=map[string]interface {}{"aMap-10":10}
55 | Blocks:
56 | block mainMenu(type="text",label="main"), from /devdump.jet
57 |
58 | ------------------------------------- dump with erroneous use
59 | dump: expected argument 0 to be a string, but got a float64
60 | ------------------------------------- dump named
61 | mainMenu:="a variable, not a block!" // string
62 | block mainMenu(type="text",label="main"), from /devdump.jet
63 | m:=map[string]interface {}{"foo":123} // map[string]interface {}
64 |
65 | done
66 |
--------------------------------------------------------------------------------
/dump_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "bytes"
19 | "reflect"
20 | "strings"
21 | "testing"
22 | )
23 |
24 | func TestDump(t *testing.T) {
25 | var b bytes.Buffer // writer for the template
26 | tmplt, err := parseSet.GetTemplate("devdump.jet") // the testing template containing dump function
27 | if err != nil {
28 | t.Log(err)
29 | t.FailNow()
30 | }
31 |
32 | // execute template with dummy inputs
33 | // MAP
34 | vars := make(VarMap)
35 | aMap := make(map[string]interface{})
36 | aMap["aMap-10"] = 10 // only one member, because map is unsorted; test could fail for no apparent reason.
37 | vars.Set("inputMap", aMap)
38 | // SLICE
39 | aSlice := []string{"sliceMember1", "sliceMember2"}
40 | vars.Set("aSlice", aSlice)
41 |
42 | // prepare dummy context
43 | ctx := struct {
44 | Name string
45 | Surname string
46 | }{Name: "John", Surname: "Doe"}
47 |
48 | // execute template
49 | err = tmplt.Execute(&b, vars, ctx)
50 | if err != nil {
51 | t.Log(err)
52 | t.FailNow()
53 | }
54 |
55 | // normalize EOL convention and
56 | // split outcome to two parts; this is necessary, because the original code
57 | // was developed on windows (SORRY !!!!)
58 | aux := strings.ReplaceAll(b.String(), "\r\n", "\n")
59 | rslt := strings.Split(aux, "===\n")
60 | if len(rslt) != 2 {
61 | t.Log("expected to get two parts, did you include separator in the template?")
62 | t.FailNow()
63 | }
64 | //t.Log(rslt[0])
65 | // compare what we got with what we wanted
66 | got := strings.Split(rslt[0], "\n")
67 | want := strings.Split(rslt[1], "\n")
68 | if !reflect.DeepEqual(got, want) {
69 | t.Errorf("\ngot :%q\nwant:%q\nAS TEXT\ngot\n%swant\n%s", got, want, rslt[0], rslt[1])
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/set_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "fmt"
19 | "reflect"
20 | "testing"
21 | )
22 |
23 | func TestSetSetExtensions(t *testing.T) {
24 | tests := [][]string{
25 | {".html.jet", ".jet"},
26 | {".tmpl", ".html"},
27 | }
28 |
29 | for _, extensions := range tests {
30 | set := NewSet(NewInMemLoader(), WithTemplateNameExtensions(extensions))
31 | if !reflect.DeepEqual(extensions, set.extensions) {
32 | t.Errorf("expected extensions %v, got %v", extensions, set.extensions)
33 | }
34 | }
35 | }
36 |
37 | func TestParseDoesNotCache(t *testing.T) {
38 | loader := NewInMemLoader()
39 | set := NewSet(loader)
40 | _, err := set.Parse("/asd.jet", `{{ foo := "bar" }}{{foo}}`)
41 | if err != nil {
42 | t.Fatalf("parsing template: %v", err)
43 | }
44 | (set.cache).(*cache).m.Range(func(_, _ interface{}) bool {
45 | t.Fatalf("template cache is not empty after Parse()")
46 | return false
47 | })
48 |
49 | loader.Set("/something_to_extend.jet", "some content to extend")
50 |
51 | _, err = set.Parse("/includes_template.jet", `{{ extends "/something_to_extend.jet" }}, and more content`)
52 | if err != nil {
53 | t.Fatalf("parsing template: %v", err)
54 | }
55 | (set.cache).(*cache).m.Range(func(_, _ interface{}) bool {
56 | t.Fatalf("template cache is not empty after Parse()")
57 | return false
58 | })
59 | }
60 |
61 | func TestGetTemplateConcurrency(t *testing.T) {
62 | l := NewInMemLoader()
63 | l.Set("foo", "{{if true}}Hi {{ .Name }}!{{end}}")
64 | set := NewSet(l)
65 |
66 | for i := 0; i < 100; i++ {
67 | t.Run(fmt.Sprintf("CC_%d", i), func(t *testing.T) {
68 | t.Parallel()
69 |
70 | _, err := set.GetTemplate("foo")
71 | if err != nil {
72 | t.Errorf("getting template from set: %v", err)
73 | }
74 | })
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/examples/asset_packaging/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // +Build ignore
16 | //go:generate go run assets/generate.go
17 | package main
18 |
19 | import (
20 | "bytes"
21 | "flag"
22 | "fmt"
23 | "io/ioutil"
24 | "log"
25 | "net/http"
26 | "os"
27 | "strings"
28 | "time"
29 |
30 | "github.com/CloudyKit/jet/v6"
31 | "github.com/CloudyKit/jet/v6/examples/asset_packaging/assets/templates"
32 | "github.com/CloudyKit/jet/v6/loaders/httpfs"
33 | )
34 |
35 | var views *jet.Set
36 |
37 | func init() {
38 | httpfsLoader, err := httpfs.NewLoader(templates.Assets)
39 | if err != nil {
40 | panic(err)
41 | }
42 |
43 | views = jet.NewSet(
44 | httpfsLoader,
45 | jet.DevelopmentMode(true), // remove or set false in production
46 | )
47 | }
48 |
49 | var runAndExit = flag.Bool("run-and-exit", false, "Run app, request / and exit (used in tests)")
50 |
51 | func main() {
52 | flag.Parse()
53 |
54 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
55 | view, err := views.GetTemplate("index.jet")
56 | if err != nil {
57 | w.WriteHeader(503)
58 | fmt.Fprintf(w, "Unexpected error while parsing template: %+v", err.Error())
59 | return
60 | }
61 | var resp bytes.Buffer
62 | if err = view.Execute(&resp, nil, nil); err != nil {
63 | w.WriteHeader(503)
64 | fmt.Fprintf(w, "Error when executing template: %+v", err.Error())
65 | return
66 | }
67 | w.WriteHeader(200)
68 | w.Write(resp.Bytes())
69 | })
70 |
71 | port := os.Getenv("PORT")
72 | if len(port) == 0 {
73 | port = ":9090"
74 | } else if !strings.HasPrefix(":", port) {
75 | port = ":" + port
76 | }
77 |
78 | log.Println("Serving on " + port)
79 | if *runAndExit {
80 | go http.ListenAndServe(port, nil)
81 | time.Sleep(1000) // wait for the server to be up
82 | resp, err := http.Get("http://localhost" + port + "/")
83 | if err != nil || resp.StatusCode != 200 {
84 | r, _ := ioutil.ReadAll(resp.Body)
85 | log.Printf("An error occurred when fetching page: %+v\n\nResponse:\n%+v\n\nStatus code: %v\n", err, string(r), resp.StatusCode)
86 | os.Exit(1)
87 | }
88 | os.Exit(0)
89 | }
90 |
91 | http.ListenAndServe(port, nil)
92 | }
93 |
--------------------------------------------------------------------------------
/examples/asset_packaging/README.md:
--------------------------------------------------------------------------------
1 | # Asset packaging example
2 |
3 | This example demonstrates how to package up your templates into your app. This is useful for your web app deployment to only consist of copying over the compiled binary.
4 |
5 | To cut down on the complexity of this example packaging up your public folder and other local assets is not shown but the process is easily extensible to incorporate these files and folders as well.
6 |
7 | To see this project in action:
8 |
9 | ```
10 | $ go get -u "github.com/shurcooL/vfsgen"
11 | $ make build
12 | ```
13 |
14 | That will build the app while compiling in the views folder in this directory. To be sure, move the compiled binary to another location, then run it from there. Access http://localhost:9090 in your browser and see that it works regardless of location.
15 |
16 | Local development is also possible. Do a `make run` in this directory, change something in the templates, refresh the browser and see it reflected there. This example is set up to use the local files in development as well as having Jet's development mode on which doesn't cache the templates – disabling the development mode when running in production is about the only thing not covered in this example because it'll depend on your app and its configuration on how this is done.
17 |
18 | Finally, for anyone looking for a step-by-step guide on how this is accomplished:
19 |
20 | 1. Add `github.com/shurcooL/vfsgen` to your project (vendoring is encouraged)
21 | 2. Add the `assets/generate.go` and `assets/templates/templates.go` files (copy the contents from this project)
22 | 3. Add `//go:generate go run assets/generate.go` to your `main.go` file (above `package main`)
23 | 4. Add a build target to your Makefile like you see in this project.
24 |
25 | Here's the rundown: when the Makefile target executes, it will first run `go generate`. This will look through the Go files in the current directory and search for annotations like you added above: `//go:generate` and run the command there. That runs the asset generation through `vfsgen` and generates the `templates_vfsdata.go` file you see when the build finishes. Through some build tags that are only set on this build (`deploy_build` in this case), only that file is included in the binary and that contains the view files as binary data.
26 |
27 | The last thing is to configure the Jet template engine via the multi loader to also use that `http.FileSystem` to look for templates – that's done in the `main.go` file.
28 |
29 | This is it, the templates are now loaded from within the binary. This process can be extended to include more directory trees – just add another folder to the assets directory, configure vfsgen in the generate.go file to fetch that directory tree and you're done. We did that in our projects with locale files as well as the whole public folder. As Gophers like to say: Just One Binary™.
30 |
--------------------------------------------------------------------------------
/dump.go:
--------------------------------------------------------------------------------
1 | package jet
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "reflect"
8 | )
9 |
10 | // dumpAll returns
11 | // - everything in Runtime.context
12 | // - everything in Runtime.variables
13 | // - everything in Runtime.set.globals
14 | // - everything in Runtime.blocks
15 | func dumpAll(a Arguments, depth int) reflect.Value {
16 | var b bytes.Buffer
17 | var vars VarMap
18 |
19 | ctx := a.runtime.context
20 | fmt.Fprintln(&b, "Context:")
21 | fmt.Fprintf(&b, "\t%s %#v\n", ctx.Type(), ctx)
22 |
23 | dumpScopeVars(&b, a.runtime.scope, 0)
24 | dumpScopeVarsToDepth(&b, a.runtime.parent, depth)
25 |
26 | vars = a.runtime.set.globals
27 | for i, name := range vars.SortedKeys() {
28 | if i == 0 {
29 | fmt.Fprintln(&b, "Globals:")
30 | }
31 | val := vars[name]
32 | fmt.Fprintf(&b, "\t%s:=%#v // %s\n", name, val, val.Type())
33 | }
34 |
35 | blockKeys := a.runtime.scope.sortedBlocks()
36 | fmt.Fprintln(&b, "Blocks:")
37 | for _, k := range blockKeys {
38 | block := a.runtime.blocks[k]
39 | dumpBlock(&b, block)
40 | }
41 |
42 | return reflect.ValueOf(b.String())
43 | }
44 |
45 | // dumpScopeVarsToDepth prints all variables in the scope, and all parent scopes,
46 | // to the limit of maxDepth.
47 | func dumpScopeVarsToDepth(w io.Writer, scope *scope, maxDepth int) {
48 | for i := 1; i <= maxDepth; i++ {
49 | if scope == nil {
50 | break // do not panic if something bad happens
51 | }
52 | dumpScopeVars(w, scope, i)
53 | scope = scope.parent
54 | }
55 | }
56 |
57 | // dumpScopeVars prints all variables in the scope.
58 | func dumpScopeVars(w io.Writer, scope *scope, lvl int) {
59 | if scope == nil {
60 | return // do not panic if something bad happens
61 | }
62 | if lvl == 0 {
63 | fmt.Fprint(w, "Variables in current scope:\n")
64 | } else {
65 | fmt.Fprintf(w, "Variables in scope %d level(s) up:\n", lvl)
66 | }
67 | vars := scope.variables
68 | for _, k := range vars.SortedKeys() {
69 | fmt.Fprintf(w, "\t%s=%#v\n", k, vars[k])
70 | }
71 | }
72 |
73 | // dumpIdentified accepts a runtime and slice of names.
74 | // Then, it prints all variables and blocks in the runtime, with names equal to one of the names
75 | // in the slice.
76 | func dumpIdentified(rnt *Runtime, ids []string) reflect.Value {
77 | var b bytes.Buffer
78 | for _, id := range ids {
79 | dumpFindVar(&b, rnt, id)
80 | dumpFindBlock(&b, rnt, id)
81 |
82 | }
83 | return reflect.ValueOf(b.String())
84 | }
85 |
86 | // dumpFindBlock finds the block by name, prints the header of the block, and name of the template in which it was defined.
87 | func dumpFindBlock(w io.Writer, rnt *Runtime, name string) {
88 | if block, ok := rnt.scope.blocks[name]; ok {
89 | dumpBlock(w, block)
90 | }
91 | }
92 |
93 | // dumpBlock prints header of the block, and template in which the block was first defined.
94 | func dumpBlock(w io.Writer, block *BlockNode) {
95 | if block == nil {
96 | return
97 | }
98 | fmt.Fprintf(w, "\tblock %s(%s), from %s\n", block.Name, block.Parameters.String(), block.TemplatePath)
99 | }
100 |
101 | // dumpFindBlock finds the variable by name, and prints the variable, if it is in the runtime.
102 | func dumpFindVar(w io.Writer, rnt *Runtime, name string) {
103 | val, err := rnt.resolve(name)
104 | if err != nil {
105 | return
106 | }
107 | fmt.Fprintf(w, "\t%s:=%#v // %s\n", name, val, val.Type())
108 | }
109 |
--------------------------------------------------------------------------------
/utils/visitor_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/CloudyKit/jet/v6"
8 | )
9 |
10 | var (
11 | Loader = jet.NewInMemLoader()
12 | Set = jet.NewSet(Loader)
13 | )
14 |
15 | func TestVisitor(t *testing.T) {
16 | var collectedIdentifiers []string
17 | Loader.Set("_testing", "{{ ident1 }}\n{{ ident2(ident3)}}\n{{ if ident4 }}\n {{ident5}}\n{{else}}\n {{ident6}}\n{{end}}\n{{ ident7|ident8|ident9+ident10|ident11[ident12]: ident13[ident14:ident15] }}")
18 | mTemplate, _ := Set.GetTemplate("_testing")
19 | Walk(mTemplate, VisitorFunc(func(context VisitorContext, node jet.Node) {
20 | if node.Type() == jet.NodeIdentifier {
21 | collectedIdentifiers = append(collectedIdentifiers, node.String())
22 | }
23 | context.Visit(node)
24 | }))
25 | if !reflect.DeepEqual(collectedIdentifiers, []string{"ident1", "ident2", "ident3", "ident4", "ident5", "ident6", "ident7", "ident8", "ident9", "ident10", "ident11", "ident12", "ident13", "ident14", "ident15"}) {
26 | t.Errorf("%q", collectedIdentifiers)
27 | }
28 | }
29 |
30 | func TestSimpleTemplate(t *testing.T) {
31 | Loader.Set("_testing2", "Thank you!\n\n\n\tHello {{userName}},\n\n\tThanks for the order!\n\n\t{{range product := products}}\n\t\t{{product.name}}\n\n\t {{block productPrice(price=product.Price) product}}\n {{if price > ExpensiveProduct}}\n Expensive!!\n {{end}}\n {{end}}\n\n\t\t${{product.price / 100}}\n\t{{end}}\n\n")
32 | mTemplate, err := Set.GetTemplate("_testing2")
33 | if err != nil {
34 | t.Error(err)
35 | }
36 |
37 | var (
38 | localVariables []string
39 | externalVariables []string
40 | )
41 |
42 | Walk(mTemplate, VisitorFunc(func(context VisitorContext, node jet.Node) {
43 |
44 | var stackState = len(localVariables) // saves the state of the local identifiers map
45 |
46 | switch node := node.(type) {
47 | case *jet.SetNode:
48 | if node.Let { // check if this is setting a new variable in the current scope
49 | for _, ident := range node.Left {
50 | // push local identifier
51 | localVariables = append(localVariables, ident.String())
52 | }
53 | }
54 | // continue checking nodes down the tree
55 | context.Visit(node)
56 | case *jet.IdentifierNode:
57 |
58 | // skip local identifiers
59 | for _, varName := range localVariables {
60 | if varName == node.Ident {
61 | return
62 | }
63 | }
64 |
65 | // skip already inserted identifiers
66 | for _, varName := range externalVariables {
67 | if varName == node.Ident {
68 | return
69 | }
70 | }
71 |
72 | // push external identifier
73 | externalVariables = append(externalVariables, node.Ident)
74 | case *jet.ActionNode:
75 | // continue without restore state of local identifiers map
76 | context.Visit(node)
77 | case *jet.BlockNode:
78 |
79 | // iterate over block parameters
80 | for _, param := range node.Parameters.List {
81 | // store block parameters in the local map
82 | localVariables = append(localVariables, param.Identifier)
83 | }
84 |
85 | // continue down tree
86 | context.Visit(node)
87 | // restore local identifiers map
88 | localVariables = localVariables[0:stackState]
89 | default:
90 | // continue down tree
91 | context.Visit(node)
92 | // restore local identifiers map
93 | localVariables = localVariables[0:stackState]
94 | }
95 |
96 | }))
97 |
98 | if !reflect.DeepEqual(externalVariables, []string{"userName", "products", "ExpensiveProduct"}) {
99 | t.Errorf("%q", externalVariables)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/examples/todos/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // +Build ignore
16 | package main
17 |
18 | import (
19 | "bytes"
20 | "encoding/base64"
21 | "fmt"
22 | "log"
23 | "net/http"
24 | "os"
25 | "reflect"
26 | "strings"
27 |
28 | "github.com/CloudyKit/jet/v6"
29 | )
30 |
31 | var views = jet.NewSet(
32 | jet.NewOSFileSystemLoader("./views"),
33 | jet.DevelopmentMode(true), // remove or set false in production
34 | )
35 |
36 | type tTODO struct {
37 | Text string
38 | Done bool
39 | }
40 |
41 | type doneTODOs struct {
42 | list map[string]*tTODO
43 | keys []string
44 | len int
45 | i int
46 | }
47 |
48 | func (dt *doneTODOs) New(todos map[string]*tTODO) *doneTODOs {
49 | dt.len = len(todos)
50 | for k := range todos {
51 | dt.keys = append(dt.keys, k)
52 | }
53 | dt.list = todos
54 | return dt
55 | }
56 |
57 | // Range satisfies the jet.Ranger interface and only returns TODOs that are done,
58 | // even when the list contains TODOs that are not done.
59 | func (dt *doneTODOs) Range() (reflect.Value, reflect.Value, bool) {
60 | for dt.i < dt.len {
61 | key := dt.keys[dt.i]
62 | dt.i++
63 | if dt.list[key].Done {
64 | return reflect.ValueOf(key), reflect.ValueOf(dt.list[key]), false
65 | }
66 | }
67 | return reflect.Value{}, reflect.Value{}, true
68 | }
69 |
70 | func (dt *doneTODOs) ProvidesIndex() bool { return true }
71 |
72 | // Render implements jet.Renderer interface
73 | func (t *tTODO) Render(r *jet.Runtime) {
74 | done := "yes"
75 | if !t.Done {
76 | done = "no"
77 | }
78 | r.Write([]byte(fmt.Sprintf("TODO: %s (done: %s)", t.Text, done)))
79 | }
80 |
81 | func main() {
82 | views.AddGlobalFunc("base64", func(a jet.Arguments) reflect.Value {
83 | a.RequireNumOfArguments("base64", 1, 1)
84 |
85 | buffer := bytes.NewBuffer(nil)
86 | fmt.Fprint(buffer, a.Get(0))
87 |
88 | return reflect.ValueOf(base64.URLEncoding.EncodeToString(buffer.Bytes()))
89 | })
90 | var todos = map[string]*tTODO{
91 | "example-todo-1": &tTODO{Text: "Add an show todo page to the example project", Done: true},
92 | "example-todo-2": &tTODO{Text: "Add an add todo page to the example project"},
93 | "example-todo-3": &tTODO{Text: "Add an update todo page to the example project"},
94 | "example-todo-4": &tTODO{Text: "Add an delete todo page to the example project", Done: true},
95 | }
96 |
97 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
98 | view, err := views.GetTemplate("todos/index.jet")
99 | if err != nil {
100 | log.Println("Unexpected template err:", err.Error())
101 | }
102 | view.Execute(w, nil, todos)
103 | })
104 | http.HandleFunc("/todo", func(w http.ResponseWriter, r *http.Request) {
105 | view, err := views.GetTemplate("todos/show.jet")
106 | if err != nil {
107 | log.Println("Unexpected template err:", err.Error())
108 | }
109 | id := r.URL.Query().Get("id")
110 | todo, ok := todos[id]
111 | if !ok {
112 | http.Redirect(w, r, "/", http.StatusNotFound)
113 | }
114 | view.Execute(w, nil, todo)
115 | })
116 | http.HandleFunc("/all-done", func(w http.ResponseWriter, r *http.Request) {
117 | view, err := views.GetTemplate("todos/index.jet")
118 | if err != nil {
119 | log.Println("Unexpected template err:", err.Error())
120 | }
121 | vars := make(jet.VarMap)
122 | vars.Set("showingAllDone", true)
123 | view.Execute(w, vars, (&doneTODOs{}).New(todos))
124 | })
125 |
126 | port := os.Getenv("PORT")
127 | if len(port) == 0 {
128 | port = ":8080"
129 | } else if !strings.HasPrefix(":", port) {
130 | port = ":" + port
131 | }
132 |
133 | log.Println("Serving on " + port)
134 | http.ListenAndServe(port, nil)
135 | }
136 |
--------------------------------------------------------------------------------
/ranger.go:
--------------------------------------------------------------------------------
1 | package jet
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "reflect"
7 | "sync"
8 | )
9 |
10 | // Ranger describes an interface for types that iterate over something.
11 | // Implementing this interface means the ranger will be used when it's
12 | // encountered on the right hand side of a range's "let" expression.
13 | type Ranger interface {
14 | // Range calls should return a key, a value and a done bool to indicate
15 | // whether there are more values to be generated.
16 | //
17 | // When the done flag is true, then the loop ends.
18 | Range() (reflect.Value, reflect.Value, bool)
19 |
20 | // ProvidesIndex should return true if keys are produced during Range()
21 | // calls. This call should be idempotent across Range() calls (i.e.
22 | // its return value must not change during an iteration).
23 | ProvidesIndex() bool
24 | }
25 |
26 | type intsRanger struct {
27 | i, val, to int64
28 | }
29 |
30 | var _ Ranger = &intsRanger{}
31 |
32 | func (r *intsRanger) Range() (index, value reflect.Value, end bool) {
33 | r.i++
34 | r.val++
35 | end = r.val == r.to
36 |
37 | // The indirection in the ValueOf calls avoids an allocation versus
38 | // using the concrete value of 'i' and 'val'. The downside is having
39 | // to interpret 'r.i' as "the current value" after Range() returns,
40 | // and so it needs to be initialized as -1.
41 | index = reflect.ValueOf(&r.i).Elem()
42 | value = reflect.ValueOf(&r.val).Elem()
43 | return
44 | }
45 |
46 | func (r *intsRanger) ProvidesIndex() bool { return true }
47 |
48 | func newIntsRanger(from, to int64) *intsRanger {
49 | r := &intsRanger{
50 | to: to,
51 | i: -1,
52 | val: from - 1,
53 | }
54 | return r
55 | }
56 |
57 | type pooledRanger interface {
58 | Ranger
59 | Setup(reflect.Value)
60 | }
61 |
62 | type sliceRanger struct {
63 | v reflect.Value
64 | i int
65 | }
66 |
67 | var _ Ranger = &sliceRanger{}
68 | var _ pooledRanger = &sliceRanger{}
69 |
70 | func (r *sliceRanger) Setup(v reflect.Value) {
71 | r.i = 0
72 | r.v = v
73 | }
74 |
75 | func (r *sliceRanger) Range() (index, value reflect.Value, end bool) {
76 | if r.i == r.v.Len() {
77 | end = true
78 | return
79 | }
80 | index = reflect.ValueOf(r.i)
81 | value = r.v.Index(r.i)
82 | r.i++
83 | return
84 | }
85 |
86 | func (r *sliceRanger) ProvidesIndex() bool { return true }
87 |
88 | type mapRanger struct {
89 | iter *reflect.MapIter
90 | hasMore bool
91 | }
92 |
93 | var _ Ranger = &mapRanger{}
94 | var _ pooledRanger = &mapRanger{}
95 |
96 | func (r *mapRanger) Setup(v reflect.Value) {
97 | r.iter = v.MapRange()
98 | r.hasMore = r.iter.Next()
99 | }
100 |
101 | func (r *mapRanger) Range() (key, value reflect.Value, end bool) {
102 | if !r.hasMore {
103 | end = true
104 | return
105 | }
106 | key, value = r.iter.Key(), r.iter.Value()
107 | r.hasMore = r.iter.Next()
108 | return
109 | }
110 |
111 | func (r *mapRanger) ProvidesIndex() bool { return true }
112 |
113 | type chanRanger struct {
114 | v reflect.Value
115 | }
116 |
117 | var _ Ranger = &chanRanger{}
118 | var _ pooledRanger = &chanRanger{}
119 |
120 | func (r *chanRanger) Setup(v reflect.Value) {
121 | r.v = v
122 | }
123 |
124 | func (r *chanRanger) Range() (_, value reflect.Value, end bool) {
125 | v, ok := r.v.Recv()
126 | value, end = v, !ok
127 | return
128 | }
129 |
130 | func (r *chanRanger) ProvidesIndex() bool { return false }
131 |
132 | // ranger pooling
133 |
134 | var (
135 | poolSliceRanger = &sync.Pool{
136 | New: func() interface{} {
137 | return new(sliceRanger)
138 | },
139 | }
140 |
141 | poolsByKind = map[reflect.Kind]*sync.Pool{
142 | reflect.Slice: poolSliceRanger,
143 | reflect.Array: poolSliceRanger,
144 | reflect.Map: &sync.Pool{
145 | New: func() interface{} {
146 | return new(mapRanger)
147 | },
148 | },
149 | reflect.Chan: &sync.Pool{
150 | New: func() interface{} {
151 | return new(chanRanger)
152 | },
153 | },
154 | }
155 | )
156 |
157 | func getRanger(v reflect.Value) (r Ranger, cleanup func(), err error) {
158 | if !v.IsValid() {
159 | return nil, nil, errors.New("can't range over invalid value")
160 | }
161 | t := v.Type()
162 | if t.Implements(rangerType) {
163 | return v.Interface().(Ranger), func() { /* no cleanup needed */ }, nil
164 | }
165 |
166 | v, isNil := indirect(v)
167 | if isNil {
168 | return nil, nil, fmt.Errorf("cannot range over nil pointer/interface (%s)", t)
169 | }
170 |
171 | pool, ok := poolsByKind[v.Kind()]
172 | if !ok {
173 | return nil, nil, fmt.Errorf("value %v (type %s) is not rangeable", v, t)
174 | }
175 |
176 | pr := pool.Get().(pooledRanger)
177 | pr.Setup(v)
178 | return pr, func() { pool.Put(pr) }, nil
179 | }
180 |
--------------------------------------------------------------------------------
/docs/changes.md:
--------------------------------------------------------------------------------
1 | # Breaking Changes
2 |
3 | ## v6
4 |
5 | When udpating from version 5 to version 6, there are breaking changes to the Go API:
6 |
7 | - Set's LoadTemplate() method was removed
8 |
9 | LoadTemplate() (which used to parse and cache a template while bypassing the Set's Loader) is removed in favor of the [new in-memory Loader](https://godoc.org/github.com/CloudyKit/jet#InMemLoader) where you can add templates on-the-fly (which is also used for tests without template files). Using it together with a file Loader via a MultiLoader restores the previous functionality: having template files accessed via the Loader and purely in-memory templates on top of those. [#182](https://github.com/CloudyKit/jet/pull/182)
10 |
11 | - Loader interface changed
12 |
13 | A Loader's Exists() method does not return the path to the template if it was found. Jet doesn't really care about what path the Loader implementation uses to locate the template. Jet expects the Loader to guarantee that the path it tried in Exists() to also work in calls to Open(), when the Exists() call returned true. [#183](https://github.com/CloudyKit/jet/pull/183)
14 |
15 | - a new Cache interface was introduced
16 |
17 | Previously, it was impossible to control if and how long a Set caches templates it parsed after fetching them via the Loader. Now, you can pass a custom Cache implementation to have complete control over caching behavior, for example to invalidate a cached template and making a Set re-fetch it via the Loader and re-parse it. [#183](https://github.com/CloudyKit/jet/pull/183)
18 |
19 | - new NewSet() with option functions
20 |
21 | The different functions used to create a Set (NewSet, NewSetLoader, NewHTMLSet, NewHTMLSetLoader()) have been removed in favor of a single NewSet() function that requires only a loader and accepts any number of configuration options in the form of option functions. When not passing any options, NewSet() will now use the HTML safe writer by default.
22 |
23 | - SetDevelopmentMode(), Delims(), SetExtensions() converted to option functions
24 |
25 | The new InDevelopmentMode(), WithDelims() and WithTemplateNameExtensions() option functions replace the previous functions. This means you can't change these settings after the Set is created, which was very likely not a good idea anyway.
26 |
27 | If you toggle development mode after Set creation, you can now use a custom Cache to configure cache-use on the fly. Since this is all the development mode does anyway, the InDevelopmentMode() option might be removed in a future major version of Jet.
28 |
29 | There are no breaking changes to the template language.
30 |
31 | ## v5
32 |
33 | When updating from version 4 to version 5, there is a breaking change:
34 |
35 | - `_` became a reserved symbol
36 |
37 | Version 5 uses `_` for two new features: it adds Go-like discard syntax in assignments (assigning anything to `_` will make jet skip the assignment) and to denote the [argument slot for the piped value](./syntax.md#piped-argument-slot). When assigning to `_`, Jet will still always evaluate the corresponding right-hand side of the assignment statement, i.e. you can use `_` to call a function but throw away its return value.
38 |
39 | When you assign (and/or use) a variable called `_` in your code, you will have to rename this variable.
40 |
41 | ## v4
42 |
43 | When updating from version 3 to version 4, there are a few breaking changes:
44 |
45 | - one-variable assignment in `range`
46 |
47 | `range x := someSlice` would set `x` to the value of the element in v3. In v4, `x` will be the index of the element. (Ranging over a channel didn't change.)
48 | See https://github.com/CloudyKit/jet/issues/158.
49 |
50 | - Runtime.Set()
51 |
52 | In v3, Set() would initialise a new variable in the top scope if no variable with that name existed. In v4, Set() will return an error when trying to set a variable that doesn't exist. Let() now always sets a variable in the current scope (possibly shadowing an existing one in a parent scope). SetOrLet() will try to change the value of an existing variable and only initialize a new variable in the current scope, if the variable doesn't exist. LetGlobal() is like Let() but always acts on the top scope.
53 |
54 | - new keywords `return`, `try`, `catch` and builtins `exec`, `ints`, `slice`, `array`
55 |
56 | `return`, `try`, `catch`, `exec`, `ints`, `slice` and `array` are now keywords or predefined identifiers. If you previously used `return`, `try` or `catch`, you will have to rename your variables. `exec`, `ints`, `slice` and `array` can technically be overwritten, but you should make sure not to name your things those words regardless.
57 |
58 | - OSFileSystemLoader only handles a single directory
59 |
60 | Use loaders.Multi to load templates from multiple directories. See https://github.com/CloudyKit/jet/issues/128.
61 |
62 | - relative paths
63 |
64 | Relative paths to templates are now handled correctly. See https://github.com/CloudyKit/jet/issues/127.
--------------------------------------------------------------------------------
/parse_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "bytes"
19 | "io/ioutil"
20 | "path"
21 | "strings"
22 | "testing"
23 | )
24 |
25 | var parseSet = NewSet(NewOSFileSystemLoader("./testData"), WithSafeWriter(nil))
26 |
27 | type ParserTestCase struct {
28 | *testing.T
29 | set *Set
30 | }
31 |
32 | func (t ParserTestCase) ExpectPrintName(name, input, output string) {
33 | set := parseSet
34 | if t.set != nil {
35 | set = t.set
36 | }
37 | template, err := set.parse(name, input, false)
38 | if err != nil {
39 | t.Errorf("%q %s", input, err.Error())
40 | return
41 | }
42 | expected := strings.Replace(template.String(), "\r\n", "\n", -1)
43 | output = strings.Replace(output, "\r\n", "\n", -1)
44 | if expected != output {
45 | t.Errorf("Unexpected tree on %s Got:\n%s\nExpected: \n%s\n", name, expected, output)
46 | }
47 | }
48 |
49 | func (t ParserTestCase) ExpectPrint(input, output string) {
50 | t.ExpectPrintName("", input, output)
51 | }
52 |
53 | func (t ParserTestCase) ExpectError(name, input, errorMessage string) {
54 | set := parseSet
55 | if t.set != nil {
56 | set = t.set
57 | }
58 | _, err := set.parse(name, input, false)
59 | if err == nil {
60 | t.Errorf("expected %q but got no error", errorMessage)
61 | return
62 | }
63 | if err.Error() != errorMessage {
64 | t.Errorf("expected %q but got %q", errorMessage, err.Error())
65 | }
66 | }
67 |
68 | func (t ParserTestCase) TestPrintFile(file string) {
69 | content, err := ioutil.ReadFile(path.Join("./testData", file))
70 | if err != nil {
71 | t.Errorf("file %s not found", file)
72 | return
73 | }
74 | parts := bytes.Split(content, []byte("==="))
75 | t.ExpectPrintName(file, string(bytes.TrimSpace(parts[0])), string(bytes.TrimSpace(parts[1])))
76 | }
77 |
78 | func (t ParserTestCase) ExpectPrintSame(input string) {
79 | t.ExpectPrint(input, input)
80 | }
81 |
82 | func TestParseTemplateAndImport(t *testing.T) {
83 | p := ParserTestCase{T: t}
84 | p.TestPrintFile("extends.jet")
85 | p.TestPrintFile("imports.jet")
86 | }
87 |
88 | func TestUsefulErrorOnLateImportOrExtends(t *testing.T) {
89 | p := ParserTestCase{T: t}
90 | p.ExpectError("late_import.jet", `{{import "./foo.jet"}}`, "template: late_import.jet:1: parsing command: unexpected keyword 'import' ('import' statements must be at the beginning of the template)")
91 | p.ExpectError("late_extends.jet", `{{extends "./foo.jet"}}`, "template: late_extends.jet:1: parsing command: unexpected keyword 'extends' ('extends' statements must be at the beginning of the template)")
92 | }
93 |
94 | func TestKeywordsDisallowedAsBlockNames(t *testing.T) {
95 | p := ParserTestCase{T: t}
96 | p.ExpectError("block_content.jet", `{{ block content() }}bla{{ end }}`, "template: block_content.jet:1: parsing block clause: unexpected keyword 'content' (expected name)")
97 | p.ExpectError("block_if.jet", `{{ block if() }}bla{{ end }}`, "template: block_if.jet:1: parsing block clause: unexpected keyword 'if' (expected name)")
98 | }
99 |
100 | func TestParseTemplateControl(t *testing.T) {
101 | p := ParserTestCase{T: t}
102 | p.TestPrintFile("if.jet")
103 | p.TestPrintFile("range.jet")
104 | }
105 |
106 | func TestParseTemplateExpressions(t *testing.T) {
107 | p := ParserTestCase{T: t}
108 | p.TestPrintFile("simple_expression.jet")
109 | p.TestPrintFile("additive_expression.jet")
110 | p.TestPrintFile("multiplicative_expression.jet")
111 | }
112 |
113 | func TestParseTemplateBlockYield(t *testing.T) {
114 | p := ParserTestCase{T: t}
115 | p.TestPrintFile("block_yield.jet")
116 | p.TestPrintFile("new_block_yield.jet")
117 | }
118 |
119 | func TestParseTemplateIndexSliceExpression(t *testing.T) {
120 | p := ParserTestCase{T: t}
121 | p.TestPrintFile("index_slice_expression.jet")
122 | }
123 |
124 | func TestParseTemplateAssignment(t *testing.T) {
125 | p := ParserTestCase{T: t}
126 | p.TestPrintFile("assignment.jet")
127 | }
128 |
129 | func TestParseTemplateWithCustomDelimiters(t *testing.T) {
130 | set := NewSet(
131 | NewOSFileSystemLoader("./testData"),
132 | WithSafeWriter(nil),
133 | WithDelims("[[", "]]"),
134 | WithCommentDelims("[*", "*]"),
135 | )
136 | p := ParserTestCase{T: t, set: set}
137 | p.TestPrintFile("custom_delimiters.jet")
138 | }
139 |
--------------------------------------------------------------------------------
/docs/builtins.md:
--------------------------------------------------------------------------------
1 | # Built-ins
2 |
3 | - [Functions](#functions)
4 | - [From Go](#from-go)
5 | - [len](#len)
6 | - [isset](#isset)
7 | - [exec](#exec)
8 | - [ints](#ints)
9 | - [dump](#dump)
10 | - [SafeWriter](#safewriter)
11 | - [safeHtml](#safehtml)
12 | - [safeJs](#safejs)
13 | - [raw/unsafe](#rawunsafe)
14 | - [Renderer](#renderer)
15 | - [writeJson](#writejson)
16 |
17 | ## Functions
18 |
19 | ### From Go
20 |
21 | The following functions simply expose functions from Go's standard library for convenience:
22 |
23 | - `lower`: exposes Go's [strings.ToLower](https://golang.org/pkg/strings/#ToLower)
24 | - `upper`: exposes Go's [strings.ToUpper](https://golang.org/pkg/strings/#ToUpper)
25 | - `hasPrefix`: exposes Go's [strings.HasPrefix](https://golang.org/pkg/strings/#HasPrefix)
26 | - `hasSuffix`: exposes Go's [strings.HasSuffix](https://golang.org/pkg/strings/#HasSuffix)
27 | - `repeat`: exposes Go's [strings.Repeat](https://golang.org/pkg/strings/#Repeat)
28 | - `replace`: exposes Go's [strings.Replace](https://golang.org/pkg/strings/#Replace)
29 | - `split`: exposes Go's [strings.Split](https://golang.org/pkg/strings/#Split)
30 | - `trimSpace`: exposes Go's [strings.TrimSpace](https://golang.org/pkg/strings/#TrimSpace)
31 | - `html`: exposes Go's [html.EscapeString](https://golang.org/pkg/html/#EscapeString)
32 | - `url`: exposes Go's [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)
33 | - `json`: exposes Go's [json.Marshal](https://golang.org/pkg/encoding/json/#Marshal)
34 |
35 | ### len
36 |
37 | `len()` takes one argument and returns the length of a string, array, slice or map, the number of fields in a struct, or the buffer size of a channel, depending on the argument's type. (Think of it like Go's `len()` function.)
38 |
39 | It panics if you pass a value of any type other than string, array, slice, map, struct or channel.
40 |
41 | `len()` indirects through arbitrary layers of pointer and interface types before checking for a valid type.
42 |
43 | ### isset
44 |
45 | `isset()` takes an arbitrary number of index, field, chain or identifier expressions and returns true if all expressions evaluate to non-nil values. It panics only when an unexpected expression type is passed in.
46 |
47 | ### exec
48 |
49 | `exec()` takes a template path and optionally a value to use as context and executes the template with the current or specified context. It returns the last value returned using the `return` statement, or nil if no `return` statement was executed.
50 |
51 | ### ints
52 |
53 | `ints()` takes two integers as lower and upper limit and returns a Ranger producing all the integers between them, including the lower and excluding the upper limit. It panics when the arguments can't be converted to integers or when the upper limit is not strictly greater than the lower limit.
54 |
55 | ### dump
56 |
57 | `dump` is meant to aid in template development, and can be used to print out variables, blocks, context, and globals that are available to the template.
58 | The function can be used in three forms:
59 |
60 | `dump()` used without parameters will print out context, variables, globals, and blocks (in this order) in the current scope, without accessing any parent.
61 |
62 | `dump(levels)` - where `levels` is an **integer** - is the same as `dump()`, but will additionally recurse over context parents to the maximum depth of `levels`.
63 | For example, `dump(1)` will additionaly print out all variables accessible in the direct parent of the current context.
64 |
65 | `dump("name1", "name2", ...)` will search for the variable and/or block with the given name(s) in any scope (current and all parents) of the current runtime.
66 |
67 | ## SafeWriter
68 |
69 | Jet includes a [`SafeWriter`](https://pkg.go.dev/github.com/CloudyKit/jet/v5?tab=doc#SafeWriter) function type for writing directly to the render output stream. This can be used to circumvent Jet's default HTML escaping. Jet has a few such functions built-in.
70 |
71 | ### safeHtml
72 |
73 | `safeHtml` is an alias for Go's [template.HTMLEscape](https://golang.org/pkg/text/template/#HTMLEscape) (converted to the `SafeWriter` type). This is the same escape function that's also applied to the evalutation result of action nodes by default. It escapes everything that could be interpreted as HTML.
74 |
75 | ### safeJs
76 |
77 | `safeJs` is an alias for Go's [template.JSEscape](https://golang.org/pkg/text/template/#JSEscape). It escapes data to be safe to use in a Javascript context.
78 |
79 | ### raw/unsafe
80 |
81 | `raw` (alias `unsafe`) is a writer that escapes nothing at all, allowing you to circumvent Jet's default HTML escaping. Use with caution!
82 |
83 | ## Renderer
84 |
85 | Jet exports a [`Renderer`](https://pkg.go.dev/github.com/CloudyKit/jet/v5?tab=doc#Renderer) interface (and [`RendererFunc`](https://pkg.go.dev/github.com/CloudyKit/jet/v5?tab=doc#RendererFunc) type which implements the interface). When an action evaluates to a value implementinng this interface, it will not be rendered using [fastprinter](https://github.com/CloudyKit/fastprinter), but by calling its Render() function instead.
86 |
87 | #### writeJson
88 |
89 | `writeJson` renders the JSON encoding of whatever you pass in to the output, escaping only "<", ">", and "&" (just like the `json` function).
90 |
--------------------------------------------------------------------------------
/loader.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "io"
21 | "io/ioutil"
22 | "os"
23 | "path"
24 | "path/filepath"
25 | "sync"
26 | )
27 |
28 | // Loader is a minimal interface required for loading templates.
29 | //
30 | // Jet will build an absolute path (with slash delimiters) before looking up templates by resolving paths in extends/import/include statements:
31 | //
32 | // - `{{ extends "/bar.jet" }}` will make Jet look up `/bar.jet` in the Loader unchanged, no matter where it occurs (since it's an absolute path)
33 | // - `{{ include("\views\bar.jet") }}` will make Jet look up `/views/bar.jet` in the Loader, no matter where it occurs
34 | // - `{{ import "bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet`
35 | // - `{{ extends "./bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet`
36 | // - `{{ import "../views\bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet`
37 | // - `{{ include("../bar.jet") }}` in `/views/foo.jet` will result in a lookup of `/bar.jet`
38 | // - `{{ import "../views/../bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/bar.jet`
39 | //
40 | // This means that the same template will always be looked up using the same path.
41 | //
42 | // Jet will also try appending multiple file endings for convenience: `{{ extends "/bar" }}` will lookup `/bar`, `/bar.jet`,
43 | // `/bar.html.jet` and `/bar.jet.html` (in that order). To avoid unneccessary lookups, use the full file name in your templates (so the first lookup
44 | // is always a hit, or override this list of extensions using Set.SetExtensions().
45 | type Loader interface {
46 | // Exists returns whether or not a template exists under the requested path.
47 | Exists(templatePath string) bool
48 |
49 | // Open returns the template's contents or an error if something went wrong.
50 | // Calls to Open() will always be preceded by a call to Exists() with the same `templatePath`.
51 | // It is the caller's duty to close the template.
52 | Open(templatePath string) (io.ReadCloser, error)
53 | }
54 |
55 | // OSFileSystemLoader implements Loader interface using OS file system (os.File).
56 | type OSFileSystemLoader struct {
57 | dir string
58 | }
59 |
60 | // compile time check that we implement Loader
61 | var _ Loader = (*OSFileSystemLoader)(nil)
62 |
63 | // NewOSFileSystemLoader returns an initialized OSFileSystemLoader.
64 | func NewOSFileSystemLoader(dirPath string) *OSFileSystemLoader {
65 | return &OSFileSystemLoader{
66 | dir: filepath.FromSlash(dirPath),
67 | }
68 | }
69 |
70 | // Exists returns true if a file is found under the template path after converting it to a file path
71 | // using the OS's path seperator and joining it with the loader's directory path.
72 | func (l *OSFileSystemLoader) Exists(templatePath string) bool {
73 | templatePath = filepath.Join(l.dir, filepath.FromSlash(templatePath))
74 | stat, err := os.Stat(templatePath)
75 | if err == nil && !stat.IsDir() {
76 | return true
77 | }
78 | return false
79 | }
80 |
81 | // Open returns the result of `os.Open()` on the file located using the same logic as Exists().
82 | func (l *OSFileSystemLoader) Open(templatePath string) (io.ReadCloser, error) {
83 | return os.Open(filepath.Join(l.dir, filepath.FromSlash(templatePath)))
84 | }
85 |
86 | // InMemLoader is a simple in-memory loader storing template contents in a simple map.
87 | // InMemLoader normalizes paths passed to its methods by converting any input path to a slash-delimited path,
88 | // turning it into an absolute path by prepending a "/" if neccessary, and cleaning it (see path.Clean()).
89 | // It is safe for concurrent use.
90 | type InMemLoader struct {
91 | lock sync.RWMutex
92 | files map[string][]byte
93 | }
94 |
95 | // compile time check that we implement Loader
96 | var _ Loader = (*InMemLoader)(nil)
97 |
98 | // NewInMemLoader return a new InMemLoader.
99 | func NewInMemLoader() *InMemLoader {
100 | return &InMemLoader{
101 | files: map[string][]byte{},
102 | }
103 | }
104 |
105 | func (l *InMemLoader) normalize(templatePath string) string {
106 | templatePath = filepath.ToSlash(templatePath)
107 | return path.Join("/", templatePath)
108 | }
109 |
110 | // Open returns a template's contents, or an error if no template was added under this path using Set().
111 | func (l *InMemLoader) Open(templatePath string) (io.ReadCloser, error) {
112 | templatePath = l.normalize(templatePath)
113 | l.lock.RLock()
114 | defer l.lock.RUnlock()
115 | f, ok := l.files[templatePath]
116 | if !ok {
117 | return nil, fmt.Errorf("%s does not exist", templatePath)
118 | }
119 |
120 | return ioutil.NopCloser(bytes.NewReader(f)), nil
121 | }
122 |
123 | // Exists returns whether or not a template is indexed under this path.
124 | func (l *InMemLoader) Exists(templatePath string) bool {
125 | templatePath = l.normalize(templatePath)
126 | l.lock.RLock()
127 | defer l.lock.RUnlock()
128 | _, ok := l.files[templatePath]
129 | return ok
130 | }
131 |
132 | // Set adds a template to the loader.
133 | func (l *InMemLoader) Set(templatePath, contents string) {
134 | templatePath = l.normalize(templatePath)
135 | l.lock.Lock()
136 | defer l.lock.Unlock()
137 | l.files[templatePath] = []byte(contents)
138 | }
139 |
140 | // Delete removes whatever contents are stored under the given path.
141 | func (l *InMemLoader) Delete(templatePath string) {
142 | templatePath = l.normalize(templatePath)
143 | l.lock.Lock()
144 | defer l.lock.Unlock()
145 | delete(l.files, templatePath)
146 | }
147 |
--------------------------------------------------------------------------------
/lex_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import "testing"
18 |
19 | func lexerTestCaseCustomLexer(t *testing.T, lexer *lexer, input string, items ...itemType) {
20 | t.Helper()
21 | for i := 0; i < len(items); i++ {
22 | item := lexer.nextItem()
23 |
24 | for item.typ == itemSpace {
25 | item = lexer.nextItem()
26 | }
27 |
28 | if item.typ != items[i] {
29 | t.Errorf("Unexpected token %s on input on %q => %q", item, input, input[item.pos:])
30 | return
31 | }
32 | }
33 | item := lexer.nextItem()
34 | if item.typ != itemEOF {
35 | t.Errorf("Unexpected token %s, expected EOF", item)
36 | }
37 | }
38 |
39 | func lexerTestCase(t *testing.T, input string, items ...itemType) {
40 | lexer := lex("test.flowRender", input, true)
41 | lexerTestCaseCustomLexer(t, lexer, input, items...)
42 | }
43 |
44 | func lexerTestCaseCustomDelimiters(t *testing.T, leftDelim, rightDelim, input string, items ...itemType) {
45 | lexer := lex("test.customDelimiters", input, false)
46 | lexer.setDelimiters(leftDelim, rightDelim)
47 | lexer.run()
48 | lexerTestCaseCustomLexer(t, lexer, input, items...)
49 | }
50 |
51 | func TestLexer(t *testing.T) {
52 | lexerTestCase(t, `{{}}`, itemLeftDelim, itemRightDelim)
53 | lexerTestCase(t, `{{- -}}`, itemLeftDelim, itemRightDelim)
54 | lexerTestCase(t, ` {{- -}} `, itemLeftDelim, itemRightDelim)
55 | lexerTestCase(t, `{{ line }}`, itemLeftDelim, itemIdentifier, itemRightDelim)
56 | lexerTestCase(t, ` {{- line -}} `, itemLeftDelim, itemIdentifier, itemRightDelim)
57 | lexerTestCase(t, `{{ . }}`, itemLeftDelim, itemIdentifier, itemRightDelim)
58 | lexerTestCase(t, `{{ .Field }}`, itemLeftDelim, itemField, itemRightDelim)
59 | lexerTestCase(t, `{{ "value" }}`, itemLeftDelim, itemString, itemRightDelim)
60 | lexerTestCase(t, `{{ call: value }}`, itemLeftDelim, itemIdentifier, itemColon, itemIdentifier, itemRightDelim)
61 | lexerTestCase(t, `{{.Ex+1}}`, itemLeftDelim, itemField, itemAdd, itemNumber, itemRightDelim)
62 | lexerTestCase(t, `{{.Ex-1}}`, itemLeftDelim, itemField, itemMinus, itemNumber, itemRightDelim)
63 | lexerTestCase(t, `{{.Ex*1}}`, itemLeftDelim, itemField, itemMul, itemNumber, itemRightDelim)
64 | lexerTestCase(t, `{{.Ex/1}}`, itemLeftDelim, itemField, itemDiv, itemNumber, itemRightDelim)
65 | lexerTestCase(t, `{{.Ex%1}}`, itemLeftDelim, itemField, itemMod, itemNumber, itemRightDelim)
66 | lexerTestCase(t, `{{.Ex=1}}`, itemLeftDelim, itemField, itemAssign, itemNumber, itemRightDelim)
67 | lexerTestCase(t, `{{Ex:=1}}`, itemLeftDelim, itemIdentifier, itemAssign, itemNumber, itemRightDelim)
68 | lexerTestCase(t, `{{.Ex!1}}`, itemLeftDelim, itemField, itemNot, itemNumber, itemRightDelim)
69 | lexerTestCase(t, `{{.Ex==1}}`, itemLeftDelim, itemField, itemEquals, itemNumber, itemRightDelim)
70 | lexerTestCase(t, `{{.Ex&&1}}`, itemLeftDelim, itemField, itemAnd, itemNumber, itemRightDelim)
71 | lexerTestCase(t, `{{ _ = foo }}`, itemLeftDelim, itemUnderscore, itemAssign, itemIdentifier, itemRightDelim)
72 | }
73 |
74 | func TestCustomDelimiters(t *testing.T) {
75 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[]]`, itemLeftDelim, itemRightDelim)
76 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ line ]]`, itemLeftDelim, itemIdentifier, itemRightDelim)
77 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ . ]]`, itemLeftDelim, itemIdentifier, itemRightDelim)
78 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ .Field ]]`, itemLeftDelim, itemField, itemRightDelim)
79 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ "value" ]]`, itemLeftDelim, itemString, itemRightDelim)
80 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ call: value ]]`, itemLeftDelim, itemIdentifier, itemColon, itemIdentifier, itemRightDelim)
81 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex+1]]`, itemLeftDelim, itemField, itemAdd, itemNumber, itemRightDelim)
82 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex-1]]`, itemLeftDelim, itemField, itemMinus, itemNumber, itemRightDelim)
83 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex*1]]`, itemLeftDelim, itemField, itemMul, itemNumber, itemRightDelim)
84 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex/1]]`, itemLeftDelim, itemField, itemDiv, itemNumber, itemRightDelim)
85 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex%1]]`, itemLeftDelim, itemField, itemMod, itemNumber, itemRightDelim)
86 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex=1]]`, itemLeftDelim, itemField, itemAssign, itemNumber, itemRightDelim)
87 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[Ex:=1]]`, itemLeftDelim, itemIdentifier, itemAssign, itemNumber, itemRightDelim)
88 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex!1]]`, itemLeftDelim, itemField, itemNot, itemNumber, itemRightDelim)
89 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex==1]]`, itemLeftDelim, itemField, itemEquals, itemNumber, itemRightDelim)
90 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex&&1]]`, itemLeftDelim, itemField, itemAnd, itemNumber, itemRightDelim)
91 | }
92 |
93 | func TestLexNegatives(t *testing.T) {
94 | lexerTestCase(t, `{{ -1 }}`, itemLeftDelim, itemNumber, itemRightDelim)
95 | lexerTestCase(t, `{{ 5 + -1 }}`, itemLeftDelim, itemNumber, itemAdd, itemNumber, itemRightDelim)
96 | lexerTestCase(t, `{{ 5 * -1 }}`, itemLeftDelim, itemNumber, itemMul, itemNumber, itemRightDelim)
97 | lexerTestCase(t, `{{ 5 / +1 }}`, itemLeftDelim, itemNumber, itemDiv, itemNumber, itemRightDelim)
98 | lexerTestCase(t, `{{ 5 % -1 }}`, itemLeftDelim, itemNumber, itemMod, itemNumber, itemRightDelim)
99 | lexerTestCase(t, `{{ 5 == -1000 }}`, itemLeftDelim, itemNumber, itemEquals, itemNumber, itemRightDelim)
100 |
101 | }
102 |
103 | func TestLexer_Bug35(t *testing.T) {
104 | lexerTestCase(t, `{{if x>y}}blahblah...{{end}}`, itemLeftDelim, itemIf, itemIdentifier, itemGreat, itemIdentifier, itemRightDelim, itemText, itemLeftDelim, itemEnd, itemRightDelim)
105 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[if x>y]]blahblah...[[end]]`, itemLeftDelim, itemIf, itemIdentifier, itemGreat, itemIdentifier, itemRightDelim, itemText, itemLeftDelim, itemEnd, itemRightDelim)
106 | }
107 |
--------------------------------------------------------------------------------
/func.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "fmt"
19 | "reflect"
20 | "time"
21 | )
22 |
23 | // Arguments holds the arguments passed to jet.Func.
24 | type Arguments struct {
25 | runtime *Runtime
26 | args CallArgs
27 | pipedVal *reflect.Value
28 | }
29 |
30 | // IsSet checks whether an argument is set or not. It behaves like the build-in isset function.
31 | func (a *Arguments) IsSet(argumentIndex int) bool {
32 | if argumentIndex < 0 {
33 | return false
34 | }
35 |
36 | if a.pipedVal != nil && !a.args.HasPipeSlot {
37 | if argumentIndex == 0 {
38 | return true
39 | }
40 | // call has an implicit first argument, so we adjust the
41 | // index before looking it up in the parsed a.args slice
42 | argumentIndex--
43 | }
44 |
45 | if argumentIndex < len(a.args.Exprs) {
46 | e := a.args.Exprs[argumentIndex]
47 | switch e.Type() {
48 | case NodeUnderscore:
49 | return a.pipedVal != nil
50 | default:
51 | return a.runtime.isSet(e)
52 | }
53 | }
54 |
55 | return false
56 | }
57 |
58 | // Get gets an argument by index.
59 | func (a *Arguments) Get(argumentIndex int) reflect.Value {
60 | if argumentIndex < 0 {
61 | return reflect.Value{}
62 | }
63 |
64 | if a.pipedVal != nil && !a.args.HasPipeSlot {
65 | if argumentIndex == 0 {
66 | return *a.pipedVal
67 | }
68 | // call has an implicit first argument, so we adjust the
69 | // index before looking it up in the parsed a.args slice
70 | argumentIndex--
71 | }
72 |
73 | if argumentIndex < len(a.args.Exprs) {
74 | e := a.args.Exprs[argumentIndex]
75 | switch e.Type() {
76 | case NodeUnderscore:
77 | return *a.pipedVal
78 | default:
79 | return a.runtime.evalPrimaryExpressionGroup(e)
80 | }
81 | }
82 |
83 | return reflect.Value{}
84 | }
85 |
86 | // Panicf panics with formatted error message.
87 | func (a *Arguments) Panicf(format string, v ...interface{}) {
88 | panic(fmt.Errorf(format, v...))
89 | }
90 |
91 | // RequireNumOfArguments panics if the number of arguments is not in the range specified by min and max.
92 | // In case there is no minimum pass -1, in case there is no maximum pass -1 respectively.
93 | func (a *Arguments) RequireNumOfArguments(funcname string, min, max int) {
94 | num := a.NumOfArguments()
95 | if min >= 0 && num < min {
96 | a.Panicf("unexpected number of arguments in a call to %s", funcname)
97 | } else if max >= 0 && num > max {
98 | a.Panicf("unexpected number of arguments in a call to %s", funcname)
99 | }
100 | }
101 |
102 | // NumOfArguments returns the number of arguments
103 | func (a *Arguments) NumOfArguments() int {
104 | num := len(a.args.Exprs)
105 | if a.pipedVal != nil && !a.args.HasPipeSlot {
106 | return num + 1
107 | }
108 | return num
109 | }
110 |
111 | // Runtime get the Runtime context
112 | func (a *Arguments) Runtime() *Runtime {
113 | return a.runtime
114 | }
115 |
116 | // ParseInto parses the arguments into the provided pointers. It returns an error if the number of pointers passed in does not
117 | // equal the number of arguments, if any argument's value is invalid according to Go's reflect package, if an argument can't
118 | // be used as the value the pointer passed in at the corresponding position points to, or if an unhandled pointer type is encountered.
119 | // Allowed pointer types are pointers to interface{}, int, int64, float64, bool, string, time.Time, reflect.Value, []interface{},
120 | // map[string]interface{}. If a pointer to a reflect.Value is passed in, the argument be assigned as-is to the value pointed to. For
121 | // pointers to int or float types, type conversion is performed automatically if necessary.
122 | func (a *Arguments) ParseInto(ptrs ...interface{}) error {
123 | if len(ptrs) < a.NumOfArguments() {
124 | return fmt.Errorf("have %d arguments, but only %d pointers to parse into", a.NumOfArguments(), len(ptrs))
125 | }
126 |
127 | for i := 0; i < a.NumOfArguments(); i++ {
128 | arg, ptr := indirectEface(a.Get(i)), ptrs[i]
129 | ok := false
130 |
131 | if !arg.IsValid() {
132 | return fmt.Errorf("argument at position %d is not a valid value", i)
133 | }
134 |
135 | switch p := ptr.(type) {
136 | case *reflect.Value:
137 | *p, ok = arg, true
138 | case *int:
139 | switch arg.Kind() {
140 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
141 | *p, ok = int(arg.Int()), true
142 | case reflect.Float32, reflect.Float64:
143 | *p, ok = int(arg.Float()), true
144 | default:
145 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr)
146 | }
147 | case *int64:
148 | switch arg.Kind() {
149 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
150 | *p, ok = arg.Int(), true
151 | case reflect.Float32, reflect.Float64:
152 | *p, ok = int64(arg.Float()), true
153 | default:
154 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr)
155 | }
156 | case *float64:
157 | switch arg.Kind() {
158 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
159 | *p, ok = float64(arg.Int()), true
160 | case reflect.Float32, reflect.Float64:
161 | *p, ok = arg.Float(), true
162 | default:
163 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr)
164 | }
165 | }
166 |
167 | if ok {
168 | continue
169 | }
170 |
171 | if !arg.CanInterface() {
172 | return fmt.Errorf("argument at position %d can't be accessed via Interface()", i)
173 | }
174 | val := arg.Interface()
175 |
176 | switch p := ptr.(type) {
177 | case *interface{}:
178 | *p, ok = val, true
179 | case *bool:
180 | *p, ok = val.(bool)
181 | case *string:
182 | *p, ok = val.(string)
183 | case *time.Time:
184 | *p, ok = val.(time.Time)
185 | case *[]interface{}:
186 | *p, ok = val.([]interface{})
187 | case *map[string]interface{}:
188 | *p, ok = val.(map[string]interface{})
189 | default:
190 | return fmt.Errorf("trying to parse %v into %v: unhandled value type %T", arg, p, val)
191 | }
192 |
193 | if !ok {
194 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr)
195 | }
196 | }
197 |
198 | return nil
199 | }
200 |
201 | // Func function implementing this type is called directly, which is faster than calling through reflect.
202 | // If a function is being called many times in the execution of a template, you may consider implementing
203 | // a wrapper for that function implementing a Func.
204 | type Func func(Arguments) reflect.Value
205 |
--------------------------------------------------------------------------------
/utils/visitor.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/CloudyKit/jet/v6"
7 | )
8 |
9 | // Walk walks the template ast and calls the Visit method on each node of the tree
10 | // if you're not familiar with the Visitor pattern please check the visitor_test.go
11 | // for usage examples
12 | func Walk(t *jet.Template, v Visitor) {
13 | v.Visit(VisitorContext{Visitor: v}, t.Root)
14 | }
15 |
16 | // Visitor type implementing the visitor pattern
17 | type Visitor interface {
18 | Visit(vc VisitorContext, node jet.Node)
19 | }
20 |
21 | // VisitorFunc a func that implements the Visitor interface
22 | type VisitorFunc func(vc VisitorContext, node jet.Node)
23 |
24 | func (visitor VisitorFunc) Visit(vc VisitorContext, node jet.Node) {
25 | visitor(vc, node)
26 | }
27 |
28 | // VisitorContext context for the current inspection
29 | type VisitorContext struct {
30 | Visitor Visitor
31 | }
32 |
33 | func (vc VisitorContext) visitNode(node jet.Node) {
34 | vc.Visitor.Visit(vc, node)
35 | }
36 |
37 | func (vc VisitorContext) Visit(node jet.Node) {
38 |
39 | switch node := node.(type) {
40 | case *jet.ListNode:
41 | vc.visitListNode(node)
42 | case *jet.ActionNode:
43 | vc.visitActionNode(node)
44 | case *jet.ChainNode:
45 | vc.visitChainNode(node)
46 | case *jet.CommandNode:
47 | vc.visitCommandNode(node)
48 | case *jet.IfNode:
49 | vc.visitIfNode(node)
50 | case *jet.PipeNode:
51 | vc.visitPipeNode(node)
52 | case *jet.RangeNode:
53 | vc.visitRangeNode(node)
54 | case *jet.BlockNode:
55 | vc.visitBlockNode(node)
56 | case *jet.IncludeNode:
57 | vc.visitIncludeNode(node)
58 | case *jet.YieldNode:
59 | vc.visitYieldNode(node)
60 | case *jet.SetNode:
61 | vc.visitSetNode(node)
62 | case *jet.AdditiveExprNode:
63 | vc.visitAdditiveExprNode(node)
64 | case *jet.MultiplicativeExprNode:
65 | vc.visitMultiplicativeExprNode(node)
66 | case *jet.ComparativeExprNode:
67 | vc.visitComparativeExprNode(node)
68 | case *jet.NumericComparativeExprNode:
69 | vc.visitNumericComparativeExprNode(node)
70 | case *jet.LogicalExprNode:
71 | vc.visitLogicalExprNode(node)
72 | case *jet.CallExprNode:
73 | vc.visitCallExprNode(node)
74 | case *jet.NotExprNode:
75 | vc.visitNotExprNode(node)
76 | case *jet.TernaryExprNode:
77 | vc.visitTernaryExprNode(node)
78 | case *jet.IndexExprNode:
79 | vc.visitIndexExprNode(node)
80 | case *jet.SliceExprNode:
81 | vc.visitSliceExprNode(node)
82 | case *jet.TextNode:
83 | case *jet.IdentifierNode:
84 | case *jet.StringNode:
85 | case *jet.NilNode:
86 | case *jet.NumberNode:
87 | case *jet.BoolNode:
88 | case *jet.FieldNode:
89 |
90 | default:
91 | panic(fmt.Errorf("unexpected node %v", node))
92 | }
93 | }
94 |
95 | func (vc VisitorContext) visitIncludeNode(includeNode *jet.IncludeNode) {
96 | vc.visitNode(includeNode)
97 | }
98 |
99 | func (vc VisitorContext) visitBlockNode(blockNode *jet.BlockNode) {
100 |
101 | for _, node := range blockNode.Parameters.List {
102 | if node.Expression != nil {
103 | vc.visitNode(node.Expression)
104 | }
105 | }
106 |
107 | if blockNode.Expression != nil {
108 | vc.visitNode(blockNode.Expression)
109 | }
110 |
111 | vc.visitListNode(blockNode.List)
112 |
113 | if blockNode.Content != nil {
114 | vc.visitNode(blockNode.Content)
115 | }
116 | }
117 |
118 | func (vc VisitorContext) visitRangeNode(rangeNode *jet.RangeNode) {
119 | vc.visitBranchNode(&rangeNode.BranchNode)
120 | }
121 |
122 | func (vc VisitorContext) visitPipeNode(pipeNode *jet.PipeNode) {
123 | for _, node := range pipeNode.Cmds {
124 | vc.visitNode(node)
125 | }
126 | }
127 |
128 | func (vc VisitorContext) visitIfNode(ifNode *jet.IfNode) {
129 | vc.visitBranchNode(&ifNode.BranchNode)
130 | }
131 | func (vc VisitorContext) visitBranchNode(branchNode *jet.BranchNode) {
132 | if branchNode.Set != nil {
133 | vc.visitNode(branchNode.Set)
134 | }
135 |
136 | if branchNode.Expression != nil {
137 | vc.visitNode(branchNode.Expression)
138 | }
139 |
140 | vc.visitNode(branchNode.List)
141 | if branchNode.ElseList != nil {
142 | vc.visitNode(branchNode.ElseList)
143 | }
144 | }
145 |
146 | func (vc VisitorContext) visitYieldNode(yieldNode *jet.YieldNode) {
147 | for _, node := range yieldNode.Parameters.List {
148 | if node.Expression != nil {
149 | vc.visitNode(node.Expression)
150 | }
151 | }
152 | if yieldNode.Expression != nil {
153 | vc.visitNode(yieldNode.Expression)
154 | }
155 | if yieldNode.Content != nil {
156 | vc.visitNode(yieldNode.Content)
157 | }
158 | }
159 |
160 | func (vc VisitorContext) visitSetNode(setNode *jet.SetNode) {
161 | for _, node := range setNode.Left {
162 | vc.visitNode(node)
163 | }
164 | for _, node := range setNode.Right {
165 | vc.visitNode(node)
166 | }
167 | }
168 |
169 | func (vc VisitorContext) visitAdditiveExprNode(additiveExprNode *jet.AdditiveExprNode) {
170 | vc.visitNode(additiveExprNode.Left)
171 | vc.visitNode(additiveExprNode.Right)
172 | }
173 |
174 | func (vc VisitorContext) visitMultiplicativeExprNode(multiplicativeExprNode *jet.MultiplicativeExprNode) {
175 | vc.visitNode(multiplicativeExprNode.Left)
176 | vc.visitNode(multiplicativeExprNode.Right)
177 | }
178 |
179 | func (vc VisitorContext) visitComparativeExprNode(comparativeExprNode *jet.ComparativeExprNode) {
180 | vc.visitNode(comparativeExprNode.Left)
181 | vc.visitNode(comparativeExprNode.Right)
182 | }
183 |
184 | func (vc VisitorContext) visitNumericComparativeExprNode(numericComparativeExprNode *jet.NumericComparativeExprNode) {
185 | vc.visitNode(numericComparativeExprNode.Left)
186 | vc.visitNode(numericComparativeExprNode.Right)
187 | }
188 |
189 | func (vc VisitorContext) visitLogicalExprNode(logicalExprNode *jet.LogicalExprNode) {
190 | vc.visitNode(logicalExprNode.Left)
191 | vc.visitNode(logicalExprNode.Right)
192 | }
193 |
194 | func (vc VisitorContext) visitCallExprNode(callExprNode *jet.CallExprNode) {
195 | vc.visitNode(callExprNode.BaseExpr)
196 | for _, node := range callExprNode.Exprs {
197 | vc.visitNode(node)
198 | }
199 | }
200 |
201 | func (vc VisitorContext) visitNotExprNode(notExprNode *jet.NotExprNode) {
202 | vc.visitNode(notExprNode.Expr)
203 | }
204 |
205 | func (vc VisitorContext) visitTernaryExprNode(ternaryExprNode *jet.TernaryExprNode) {
206 | vc.visitNode(ternaryExprNode.Boolean)
207 | vc.visitNode(ternaryExprNode.Left)
208 | vc.visitNode(ternaryExprNode.Right)
209 | }
210 |
211 | func (vc VisitorContext) visitIndexExprNode(indexNode *jet.IndexExprNode) {
212 | vc.visitNode(indexNode.Base)
213 | vc.visitNode(indexNode.Index)
214 | }
215 |
216 | func (vc VisitorContext) visitSliceExprNode(sliceExprNode *jet.SliceExprNode) {
217 | vc.visitNode(sliceExprNode.Base)
218 | vc.visitNode(sliceExprNode.Index)
219 | vc.visitNode(sliceExprNode.EndIndex)
220 | }
221 |
222 | func (vc VisitorContext) visitCommandNode(commandNode *jet.CommandNode) {
223 | vc.visitNode(commandNode.BaseExpr)
224 | for _, node := range commandNode.Exprs {
225 | vc.visitNode(node)
226 | }
227 | }
228 |
229 | func (vc VisitorContext) visitChainNode(chainNode *jet.ChainNode) {
230 | vc.visitNode(chainNode.Node)
231 | }
232 |
233 | func (vc VisitorContext) visitActionNode(actionNode *jet.ActionNode) {
234 | if actionNode.Set != nil {
235 | vc.visitNode(actionNode.Set)
236 | }
237 | if actionNode.Pipe != nil {
238 | vc.visitNode(actionNode.Pipe)
239 | }
240 | }
241 |
242 | func (vc VisitorContext) visitListNode(listNode *jet.ListNode) {
243 | for _, node := range listNode.Nodes {
244 | vc.visitNode(node)
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/default.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "encoding/json"
19 | "errors"
20 | "fmt"
21 | "html"
22 | "io"
23 | "io/ioutil"
24 | "net/url"
25 | "reflect"
26 | "strings"
27 | "text/template"
28 | )
29 |
30 | var defaultVariables map[string]reflect.Value
31 |
32 | func init() {
33 | defaultVariables = map[string]reflect.Value{
34 | "lower": reflect.ValueOf(strings.ToLower),
35 | "upper": reflect.ValueOf(strings.ToUpper),
36 | "hasPrefix": reflect.ValueOf(strings.HasPrefix),
37 | "hasSuffix": reflect.ValueOf(strings.HasSuffix),
38 | "repeat": reflect.ValueOf(strings.Repeat),
39 | "replace": reflect.ValueOf(strings.Replace),
40 | "split": reflect.ValueOf(strings.Split),
41 | "trimSpace": reflect.ValueOf(strings.TrimSpace),
42 | "html": reflect.ValueOf(html.EscapeString),
43 | "url": reflect.ValueOf(url.QueryEscape),
44 | "safeHtml": reflect.ValueOf(SafeWriter(template.HTMLEscape)),
45 | "safeJs": reflect.ValueOf(SafeWriter(template.JSEscape)),
46 | "raw": reflect.ValueOf(SafeWriter(unsafePrinter)),
47 | "unsafe": reflect.ValueOf(SafeWriter(unsafePrinter)),
48 | "writeJson": reflect.ValueOf(jsonRenderer),
49 | "json": reflect.ValueOf(json.Marshal),
50 | "map": reflect.ValueOf(newMap),
51 | "slice": reflect.ValueOf(newSlice),
52 | "array": reflect.ValueOf(newSlice),
53 | "isset": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
54 | a.RequireNumOfArguments("isset", 1, -1)
55 | for i := 0; i < a.NumOfArguments(); i++ {
56 | if !a.IsSet(i) {
57 | return valueBoolFALSE
58 | }
59 | }
60 | return valueBoolTRUE
61 | })),
62 | "len": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
63 | a.RequireNumOfArguments("len", 1, 1)
64 |
65 | expression := a.Get(0)
66 | if !expression.IsValid() {
67 | a.Panicf("len(): argument is not a valid value")
68 | }
69 | if expression.Kind() == reflect.Ptr || expression.Kind() == reflect.Interface {
70 | expression = expression.Elem()
71 | }
72 |
73 | switch expression.Kind() {
74 | case reflect.Array, reflect.Chan, reflect.Slice, reflect.Map, reflect.String:
75 | return reflect.ValueOf(expression.Len())
76 | case reflect.Struct:
77 | return reflect.ValueOf(expression.NumField())
78 | }
79 |
80 | a.Panicf("len(): invalid value type %s", expression.Type())
81 | return reflect.Value{}
82 | })),
83 | "includeIfExists": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
84 | a.RequireNumOfArguments("includeIfExists", 1, 2)
85 | t, err := a.runtime.set.GetTemplate(a.Get(0).String())
86 | // If template exists but returns an error then panic instead of failing silently
87 | if t != nil && err != nil {
88 | panic(fmt.Errorf("including %s: %w", a.Get(0).String(), err))
89 | }
90 | if err != nil {
91 | return hiddenFalse
92 | }
93 |
94 | a.runtime.newScope()
95 | defer a.runtime.releaseScope()
96 |
97 | a.runtime.blocks = t.processedBlocks
98 | root := t.Root
99 | if t.extends != nil {
100 | root = t.extends.Root
101 | }
102 |
103 | if a.NumOfArguments() > 1 {
104 | c := a.runtime.context
105 | defer func() { a.runtime.context = c }()
106 | a.runtime.context = a.Get(1)
107 | }
108 |
109 | a.runtime.executeList(root)
110 |
111 | return hiddenTrue
112 | })),
113 | "exec": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) {
114 | a.RequireNumOfArguments("exec", 1, 2)
115 | t, err := a.runtime.set.GetTemplate(a.Get(0).String())
116 | if err != nil {
117 | panic(fmt.Errorf("exec(%s, %v): %w", a.Get(0), a.Get(1), err))
118 | }
119 |
120 | a.runtime.newScope()
121 | defer a.runtime.releaseScope()
122 |
123 | w := a.runtime.Writer
124 | defer func() { a.runtime.Writer = w }()
125 | a.runtime.Writer = ioutil.Discard
126 |
127 | a.runtime.blocks = t.processedBlocks
128 | root := t.Root
129 | if t.extends != nil {
130 | root = t.extends.Root
131 | }
132 |
133 | if a.NumOfArguments() > 1 {
134 | c := a.runtime.context
135 | defer func() { a.runtime.context = c }()
136 | a.runtime.context = a.Get(1)
137 | }
138 | result = a.runtime.executeList(root)
139 |
140 | return result
141 | })),
142 | "ints": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) {
143 | var from, to int64
144 | err := a.ParseInto(&from, &to)
145 | if err != nil {
146 | panic(err)
147 | }
148 | // check to > from
149 | if to <= from {
150 | panic(errors.New("invalid range for ints ranger: 'from' must be smaller than 'to'"))
151 | }
152 | return reflect.ValueOf(newIntsRanger(from, to))
153 | })),
154 | "dump": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) {
155 | switch numArgs := a.NumOfArguments(); numArgs {
156 | case 0:
157 | // no arguments were provided, dump all; do not recurse over parents
158 | return dumpAll(a, 0)
159 | case 1:
160 | if arg := a.Get(0); arg.Kind() == reflect.Float64 {
161 | // dump all, maybe walk into parents
162 | return dumpAll(a, int(arg.Float()))
163 | }
164 | fallthrough
165 | default:
166 | // one or more arguments were provided, grab them and check they are all strings
167 | ids := make([]string, numArgs)
168 | for i := range ids {
169 | arg := a.Get(i)
170 | if arg.Kind() != reflect.String {
171 | panic(fmt.Errorf("dump: expected argument %d to be a string, but got a %T", i, arg.Interface()))
172 | }
173 | ids = append(ids, arg.String())
174 | }
175 | return dumpIdentified(a.runtime, ids)
176 | }
177 | })),
178 | }
179 | }
180 |
181 | type hiddenBool bool
182 |
183 | func (m hiddenBool) Render(r *Runtime) { /* render nothing -> hidden */ }
184 |
185 | var hiddenTrue = reflect.ValueOf(hiddenBool(true))
186 | var hiddenFalse = reflect.ValueOf(hiddenBool(false))
187 |
188 | func jsonRenderer(v interface{}) RendererFunc {
189 | return func(r *Runtime) {
190 | err := json.NewEncoder(r.Writer).Encode(v)
191 | if err != nil {
192 | panic(err)
193 | }
194 | }
195 | }
196 |
197 | func unsafePrinter(w io.Writer, b []byte) {
198 | w.Write(b)
199 | }
200 |
201 | // SafeWriter is a function that writes bytes directly to the render output, without going through Jet's auto-escaping phase.
202 | // Use/implement this if content should be escaped differently or not at all (see raw/unsafe builtins).
203 | type SafeWriter func(io.Writer, []byte)
204 |
205 | var stringType = reflect.TypeOf("")
206 |
207 | var newMap = Func(func(a Arguments) reflect.Value {
208 | if a.NumOfArguments()%2 > 0 {
209 | panic("map(): incomplete key-value pair (even number of arguments required)")
210 | }
211 |
212 | m := reflect.ValueOf(make(map[string]interface{}, a.NumOfArguments()/2))
213 |
214 | for i := 0; i < a.NumOfArguments(); i += 2 {
215 | key := a.Get(i)
216 | if !key.IsValid() {
217 | a.Panicf("map(): key argument at position %d is not a valid value!", i)
218 | }
219 | if !key.Type().ConvertibleTo(stringType) {
220 | a.Panicf("map(): can't use %+v as string key: %s is not convertible to string", key, key.Type())
221 | }
222 | key = key.Convert(stringType)
223 | m.SetMapIndex(a.Get(i), a.Get(i+1))
224 | }
225 |
226 | return m
227 | })
228 |
229 | var newSlice = Func(func(a Arguments) reflect.Value {
230 | arr := make([]interface{}, a.NumOfArguments())
231 | for i := 0; i < a.NumOfArguments(); i++ {
232 | arr[i] = a.Get(i).Interface()
233 | }
234 | return reflect.ValueOf(arr)
235 | })
236 |
--------------------------------------------------------------------------------
/set.go:
--------------------------------------------------------------------------------
1 | package jet
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "path"
8 | "path/filepath"
9 | "reflect"
10 | "sync"
11 | "text/template"
12 | )
13 |
14 | // Set is responsible to load, parse and cache templates.
15 | // Every Jet template is associated with a Set.
16 | type Set struct {
17 | loader Loader
18 | cache Cache
19 | escapee SafeWriter // escapee to use at runtime
20 | globals VarMap // global scope for this template set
21 | gmx *sync.RWMutex // global variables map mutex
22 | extensions []string
23 | developmentMode bool
24 | leftDelim string
25 | rightDelim string
26 | leftComment string
27 | rightComment string
28 | }
29 |
30 | // Option is the type of option functions that can be used in NewSet().
31 | type Option func(*Set)
32 |
33 | // NewSet returns a new Set relying on loader. NewSet panics if a nil Loader is passed.
34 | func NewSet(loader Loader, opts ...Option) *Set {
35 | if loader == nil {
36 | panic(errors.New("jet: NewSet() must not be called with a nil loader"))
37 | }
38 |
39 | s := &Set{
40 | loader: loader,
41 | cache: &cache{},
42 | escapee: template.HTMLEscape,
43 | globals: VarMap{},
44 | gmx: &sync.RWMutex{},
45 | extensions: []string{
46 | "", // in case the path is given with the correct extension already
47 | ".jet",
48 | ".html.jet",
49 | ".jet.html",
50 | },
51 | }
52 |
53 | for _, opt := range opts {
54 | opt(s)
55 | }
56 |
57 | return s
58 | }
59 |
60 | // WithCache returns an option function that sets the cache to use for template parsing results.
61 | // Use InDevelopmentMode() to disable caching of parsed templates. By default, Jet uses a
62 | // concurrency-safe in-memory cache that holds templates forever.
63 | func WithCache(c Cache) Option {
64 | if c == nil {
65 | panic(errors.New("jet: WithCache() must not be called with a nil cache"))
66 | }
67 | return func(s *Set) {
68 | s.cache = c
69 | }
70 | }
71 |
72 | // WithSafeWriter returns an option function that sets the escaping function to use when executing
73 | // templates. By default, Jet uses a writer that takes care of HTML escaping. Pass nil to disable escaping.
74 | func WithSafeWriter(w SafeWriter) Option {
75 | return func(s *Set) {
76 | s.escapee = w
77 | }
78 | }
79 |
80 | // WithDelims returns an option function that sets the delimiters to the specified strings.
81 | // Parsed templates will inherit the settings. Not setting them leaves them at the default: `{{` and `}}`.
82 | func WithDelims(left, right string) Option {
83 | return func(s *Set) {
84 | s.leftDelim = left
85 | s.rightDelim = right
86 | }
87 | }
88 |
89 | // WithCommentDelims returns an option function that sets the comment delimiters to the specified strings.
90 | // Parsed templates will inherit the settings. Not setting them leaves them at the default: `{*` and `*}`.
91 | func WithCommentDelims(left, right string) Option {
92 | return func(s *Set) {
93 | s.leftComment = left
94 | s.rightComment = right
95 | }
96 | }
97 |
98 | // WithTemplateNameExtensions returns an option function that sets the extensions to try when looking
99 | // up template names in the cache or loader. Default extensions are `""` (no extension), `".jet"`,
100 | // `".html.jet"`, `".jet.html"`. Extensions will be tried in the order they are defined in the slice.
101 | // WithTemplateNameExtensions panics when you pass in a nil or empty slice.
102 | func WithTemplateNameExtensions(extensions []string) Option {
103 | if len(extensions) == 0 {
104 | panic(errors.New("jet: WithTemplateNameExtensions() must not be called with a nil or empty slice of extensions"))
105 | }
106 | return func(s *Set) {
107 | s.extensions = extensions
108 | }
109 | }
110 |
111 | // InDevelopmentMode returns an option function that toggles development mode on, meaning the cache will
112 | // always be bypassed and every template lookup will go to the loader.
113 | func InDevelopmentMode() Option {
114 | return DevelopmentMode(true)
115 | }
116 |
117 | // DevelopmentMode returns an option function that sets development mode on or off. "On" means the cache will
118 | // always be bypassed and every template lookup will go to the loader.
119 | func DevelopmentMode(mode bool) Option {
120 | return func(s *Set) {
121 | s.developmentMode = mode
122 | }
123 | }
124 |
125 | // GetTemplate tries to find (and parse, if not yet parsed) the template at the specified path.
126 | //
127 | // For example, GetTemplate("catalog/products.list") with extensions set to []string{"", ".html.jet",".jet"}
128 | // will try to look for:
129 | // 1. catalog/products.list
130 | // 2. catalog/products.list.html.jet
131 | // 3. catalog/products.list.jet
132 | // in the set's templates cache, and if it can't find the template it will try to load the same paths via
133 | // the loader, and, if parsed successfully, cache the template (unless running in development mode).
134 | func (s *Set) GetTemplate(templatePath string) (t *Template, err error) {
135 | return s.getSiblingTemplate(templatePath, "/", true)
136 | }
137 |
138 | func (s *Set) getSiblingTemplate(templatePath, siblingPath string, cacheAfterParsing bool) (t *Template, err error) {
139 | templatePath = filepath.ToSlash(templatePath)
140 | siblingPath = filepath.ToSlash(siblingPath)
141 | if !path.IsAbs(templatePath) {
142 | siblingDir := path.Dir(siblingPath)
143 | templatePath = path.Join(siblingDir, templatePath)
144 | }
145 | return s.getTemplate(templatePath, cacheAfterParsing)
146 | }
147 |
148 | // same as GetTemplate, but doesn't cache a template when found through the loader.
149 | func (s *Set) getTemplate(templatePath string, cacheAfterParsing bool) (t *Template, err error) {
150 | if !s.developmentMode {
151 | t, found := s.getTemplateFromCache(templatePath)
152 | if found {
153 | return t, nil
154 | }
155 | }
156 |
157 | t, err = s.getTemplateFromLoader(templatePath, cacheAfterParsing)
158 | if err == nil && cacheAfterParsing && !s.developmentMode {
159 | s.cache.Put(templatePath, t)
160 | }
161 | return t, err
162 | }
163 |
164 | func (s *Set) getTemplateFromCache(templatePath string) (t *Template, ok bool) {
165 | // check path with all possible extensions in cache
166 | for _, extension := range s.extensions {
167 | canonicalPath := templatePath + extension
168 | if t := s.cache.Get(canonicalPath); t != nil {
169 | return t, true
170 | }
171 | }
172 | return nil, false
173 | }
174 |
175 | func (s *Set) getTemplateFromLoader(templatePath string, cacheAfterParsing bool) (t *Template, err error) {
176 | // check path with all possible extensions in loader
177 | for _, extension := range s.extensions {
178 | canonicalPath := templatePath + extension
179 | if found := s.loader.Exists(canonicalPath); found {
180 | return s.loadFromFile(canonicalPath, cacheAfterParsing)
181 | }
182 | }
183 | return nil, fmt.Errorf("template %s could not be found", templatePath)
184 | }
185 |
186 | func (s *Set) loadFromFile(templatePath string, cacheAfterParsing bool) (template *Template, err error) {
187 | f, err := s.loader.Open(templatePath)
188 | if err != nil {
189 | return nil, err
190 | }
191 | defer f.Close()
192 | content, err := ioutil.ReadAll(f)
193 | if err != nil {
194 | return nil, err
195 | }
196 | return s.parse(templatePath, string(content), cacheAfterParsing)
197 | }
198 |
199 | // Parse parses `contents` as if it were located at `templatePath`, but won't put the result into the cache.
200 | // Any referenced template (e.g. via `extends` or `import` statements) will be tried to be loaded from the cache.
201 | // If a referenced template has to be loaded and parsed, it will also not be put into the cache after parsing.
202 | func (s *Set) Parse(templatePath, contents string) (template *Template, err error) {
203 | templatePath = filepath.ToSlash(templatePath)
204 | switch path.Base(templatePath) {
205 | case ".", "/":
206 | return nil, errors.New("template path has no base name")
207 | }
208 | // make sure it's absolute and clean it
209 | templatePath = path.Join("/", templatePath)
210 |
211 | return s.parse(templatePath, contents, false)
212 | }
213 |
214 | // AddGlobal adds a global variable into the Set,
215 | // overriding any value previously set under the specified key.
216 | // It returns the Set it was called on to allow for method chaining.
217 | func (s *Set) AddGlobal(key string, i interface{}) *Set {
218 | s.gmx.Lock()
219 | defer s.gmx.Unlock()
220 | s.globals[key] = reflect.ValueOf(i)
221 | return s
222 | }
223 |
224 | // LookupGlobal returns the global variable previously set under the specified key.
225 | // It returns the nil interface and false if no variable exists under that key.
226 | func (s *Set) LookupGlobal(key string) (val interface{}, found bool) {
227 | s.gmx.RLock()
228 | defer s.gmx.RUnlock()
229 | val, found = s.globals[key]
230 | return
231 | }
232 |
233 | // AddGlobalFunc adds a global function into the Set,
234 | // overriding any function previously set under the specified key.
235 | // It returns the Set it was called on to allow for method chaining.
236 | func (s *Set) AddGlobalFunc(key string, fn Func) *Set {
237 | return s.AddGlobal(key, fn)
238 | }
239 |
--------------------------------------------------------------------------------
/constructors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "fmt"
19 | "strconv"
20 | "strings"
21 | )
22 |
23 | func (t *Template) newSliceExpr(pos Pos, line int, base, index, endIndex Expression) *SliceExprNode {
24 | return &SliceExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeSliceExpr, Pos: pos, Line: line}, Index: index, Base: base, EndIndex: endIndex}
25 | }
26 |
27 | func (t *Template) newIndexExpr(pos Pos, line int, base, index Expression) *IndexExprNode {
28 | return &IndexExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeIndexExpr, Pos: pos, Line: line}, Index: index, Base: base}
29 | }
30 |
31 | func (t *Template) newTernaryExpr(pos Pos, line int, boolean, left, right Expression) *TernaryExprNode {
32 | return &TernaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeTernaryExpr, Pos: pos, Line: line}, Boolean: boolean, Left: left, Right: right}
33 | }
34 |
35 | func (t *Template) newSet(pos Pos, line int, isLet, isIndexExprGetLookup bool, left, right []Expression) *SetNode {
36 | return &SetNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeSet, Pos: pos, Line: line}, Let: isLet, IndexExprGetLookup: isIndexExprGetLookup, Left: left, Right: right}
37 | }
38 |
39 | func (t *Template) newCallExpr(pos Pos, line int, expr Expression) *CallExprNode {
40 | return &CallExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeCallExpr, Pos: pos, Line: line}, BaseExpr: expr}
41 | }
42 | func (t *Template) newNotExpr(pos Pos, line int, expr Expression) *NotExprNode {
43 | return &NotExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNotExpr, Pos: pos, Line: line}, Expr: expr}
44 | }
45 | func (t *Template) newNumericComparativeExpr(pos Pos, line int, left, right Expression, item item) *NumericComparativeExprNode {
46 | return &NumericComparativeExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNumericComparativeExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}}
47 | }
48 |
49 | func (t *Template) newComparativeExpr(pos Pos, line int, left, right Expression, item item) *ComparativeExprNode {
50 | return &ComparativeExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeComparativeExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}}
51 | }
52 |
53 | func (t *Template) newLogicalExpr(pos Pos, line int, left, right Expression, item item) *LogicalExprNode {
54 | return &LogicalExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeLogicalExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}}
55 | }
56 |
57 | func (t *Template) newMultiplicativeExpr(pos Pos, line int, left, right Expression, item item) *MultiplicativeExprNode {
58 | return &MultiplicativeExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeMultiplicativeExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}}
59 | }
60 |
61 | func (t *Template) newAdditiveExpr(pos Pos, line int, left, right Expression, item item) *AdditiveExprNode {
62 | return &AdditiveExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeAdditiveExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}}
63 | }
64 |
65 | func (t *Template) newList(pos Pos) *ListNode {
66 | return &ListNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeList, Pos: pos}}
67 | }
68 |
69 | func (t *Template) newText(pos Pos, text string) *TextNode {
70 | return &TextNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeText, Pos: pos}, Text: []byte(text)}
71 | }
72 |
73 | func (t *Template) newPipeline(pos Pos, line int) *PipeNode {
74 | return &PipeNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodePipe, Pos: pos, Line: line}}
75 | }
76 |
77 | func (t *Template) newAction(pos Pos, line int) *ActionNode {
78 | return &ActionNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeAction, Pos: pos, Line: line}}
79 | }
80 |
81 | func (t *Template) newCommand(pos Pos) *CommandNode {
82 | return &CommandNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeCommand, Pos: pos}}
83 | }
84 |
85 | func (t *Template) newNil(pos Pos) *NilNode {
86 | return &NilNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNil, Pos: pos}}
87 | }
88 |
89 | func (t *Template) newField(pos Pos, line int, ident string) *FieldNode {
90 | return &FieldNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeField, Pos: pos, Line: line}, Ident: strings.Split(ident[1:], ".")} //[1:] to drop leading period
91 | }
92 |
93 | func (t *Template) newChain(pos Pos, line int, node Node) *ChainNode {
94 | return &ChainNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeChain, Pos: pos, Line: line}, Node: node}
95 | }
96 |
97 | func (t *Template) newBool(pos Pos, true bool) *BoolNode {
98 | return &BoolNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeBool, Pos: pos}, True: true}
99 | }
100 |
101 | func (t *Template) newString(pos Pos, orig, text string) *StringNode {
102 | return &StringNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeString, Pos: pos}, Quoted: orig, Text: text}
103 | }
104 |
105 | func (t *Template) newEnd(pos Pos) *endNode {
106 | return &endNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeEnd, Pos: pos}}
107 | }
108 |
109 | func (t *Template) newContent(pos Pos) *contentNode {
110 | return &contentNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeContent, Pos: pos}}
111 | }
112 |
113 | func (t *Template) newElse(pos Pos, line int) *elseNode {
114 | return &elseNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeElse, Pos: pos, Line: line}}
115 | }
116 |
117 | func (t *Template) newIf(pos Pos, line int, set *SetNode, pipe Expression, list, elseList *ListNode) *IfNode {
118 | return &IfNode{BranchNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeIf, Pos: pos, Line: line}, Set: set, Expression: pipe, List: list, ElseList: elseList}}
119 | }
120 |
121 | func (t *Template) newRange(pos Pos, line int, set *SetNode, pipe Expression, list, elseList *ListNode) *RangeNode {
122 | return &RangeNode{BranchNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeRange, Pos: pos, Line: line}, Set: set, Expression: pipe, List: list, ElseList: elseList}}
123 | }
124 |
125 | func (t *Template) newBlock(pos Pos, line int, name string, parameters *BlockParameterList, pipe Expression, listNode, contentListNode *ListNode) *BlockNode {
126 | return &BlockNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeBlock, Line: line, Pos: pos}, Name: name, Parameters: parameters, Expression: pipe, List: listNode, Content: contentListNode}
127 | }
128 |
129 | func (t *Template) newYield(pos Pos, line int, name string, bplist *BlockParameterList, pipe Expression, content *ListNode, isContent bool) *YieldNode {
130 | return &YieldNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeYield, Pos: pos, Line: line}, Name: name, Parameters: bplist, Expression: pipe, Content: content, IsContent: isContent}
131 | }
132 |
133 | func (t *Template) newInclude(pos Pos, line int, name, context Expression) *IncludeNode {
134 | return &IncludeNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeInclude, Pos: pos, Line: line}, Name: name, Context: context}
135 | }
136 |
137 | func (t *Template) newReturn(pos Pos, line int, pipe Expression) *ReturnNode {
138 | return &ReturnNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeReturn, Pos: pos, Line: line}, Value: pipe}
139 | }
140 |
141 | func (t *Template) newTry(pos Pos, line int, list *ListNode, catch *catchNode) *TryNode {
142 | return &TryNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeTry, Pos: pos, Line: line}, List: list, Catch: catch}
143 | }
144 |
145 | func (t *Template) newCatch(pos Pos, line int, errVar *IdentifierNode, list *ListNode) *catchNode {
146 | return &catchNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeCatch, Pos: pos, Line: line}, Err: errVar, List: list}
147 | }
148 |
149 | func (t *Template) newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) {
150 | n := &NumberNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNumber, Pos: pos}, Text: text}
151 | // todo: optimize
152 | switch typ {
153 | case itemCharConstant:
154 | _rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0])
155 | if err != nil {
156 | return nil, err
157 | }
158 | if tail != "'" {
159 | return nil, fmt.Errorf("malformed character constant: %s", text)
160 | }
161 | n.Int64 = int64(_rune)
162 | n.IsInt = true
163 | n.Uint64 = uint64(_rune)
164 | n.IsUint = true
165 | n.Float64 = float64(_rune) //odd but those are the rules.
166 | n.IsFloat = true
167 | return n, nil
168 | case itemComplex:
169 | //fmt.Sscan can parse the pair, so let it do the work.
170 | if _, err := fmt.Sscan(text, &n.Complex128); err != nil {
171 | return nil, err
172 | }
173 | n.IsComplex = true
174 | n.simplifyComplex()
175 | return n, nil
176 | }
177 | //Imaginary constants can only be complex unless they are zero.
178 | if len(text) > 0 && text[len(text)-1] == 'i' {
179 | f, err := strconv.ParseFloat(text[:len(text)-1], 64)
180 | if err == nil {
181 | n.IsComplex = true
182 | n.Complex128 = complex(0, f)
183 | n.simplifyComplex()
184 | return n, nil
185 | }
186 | }
187 | // Do integer test first so we get 0x123 etc.
188 | u, err := strconv.ParseUint(text, 0, 64) // will fail for -0; fixed below.
189 | if err == nil {
190 | n.IsUint = true
191 | n.Uint64 = u
192 | }
193 | i, err := strconv.ParseInt(text, 0, 64)
194 | if err == nil {
195 | n.IsInt = true
196 | n.Int64 = i
197 | if i == 0 {
198 | n.IsUint = true // in case of -0.
199 | n.Uint64 = u
200 | }
201 | }
202 | // If an integer extraction succeeded, promote the float.
203 | if n.IsInt {
204 | n.IsFloat = true
205 | n.Float64 = float64(n.Int64)
206 | } else if n.IsUint {
207 | n.IsFloat = true
208 | n.Float64 = float64(n.Uint64)
209 | } else {
210 | f, err := strconv.ParseFloat(text, 64)
211 | if err == nil {
212 | // If we parsed it as a float but it looks like an integer,
213 | // it's a huge number too large to fit in an int. Reject it.
214 | if !strings.ContainsAny(text, ".eE") {
215 | return nil, fmt.Errorf("integer overflow: %q", text)
216 | }
217 | n.IsFloat = true
218 | n.Float64 = f
219 | // If a floating-point extraction succeeded, extract the int if needed.
220 | if !n.IsInt && float64(int64(f)) == f {
221 | n.IsInt = true
222 | n.Int64 = int64(f)
223 | }
224 | if !n.IsUint && float64(uint64(f)) == f {
225 | n.IsUint = true
226 | n.Uint64 = uint64(f)
227 | }
228 | }
229 | }
230 |
231 | if !n.IsInt && !n.IsUint && !n.IsFloat {
232 | return nil, fmt.Errorf("illegal number syntax: %q", text)
233 | }
234 |
235 | return n, nil
236 | }
237 |
238 | func (t *Template) newIdentifier(ident string, pos Pos, line int) *IdentifierNode {
239 | return &IdentifierNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeIdentifier, Pos: pos, Line: line}, Ident: ident}
240 | }
241 |
242 | func (t *Template) newUnderscore(pos Pos, line int) *UnderscoreNode {
243 | return &UnderscoreNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeUnderscore, Pos: pos, Line: line}}
244 | }
245 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/node.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "path/filepath"
21 | )
22 |
23 | var textFormat = "%s" //Changed to "%q" in tests for better error messages.
24 |
25 | type Node interface {
26 | Type() NodeType
27 | String() string
28 | Position() Pos
29 | line() int
30 | error(error)
31 | errorf(string, ...interface{})
32 | }
33 |
34 | type Expression interface {
35 | Node
36 | }
37 |
38 | // Pos represents a byte position in the original input text from which
39 | // this template was parsed.
40 | type Pos int
41 |
42 | func (p Pos) Position() Pos {
43 | return p
44 | }
45 |
46 | // NodeType identifies the type of a parse tree node.
47 | type NodeType int
48 |
49 | type NodeBase struct {
50 | TemplatePath string
51 | Line int
52 | NodeType
53 | Pos
54 | }
55 |
56 | func (node *NodeBase) line() int {
57 | return node.Line
58 | }
59 |
60 | func (node *NodeBase) error(err error) {
61 | node.errorf("%s", err)
62 | }
63 |
64 | func (node *NodeBase) errorf(format string, v ...interface{}) {
65 | panic(fmt.Errorf("Jet Runtime Error (%q:%d): %s", filepath.ToSlash(node.TemplatePath), node.Line, fmt.Sprintf(format, v...)))
66 | }
67 |
68 | // Type returns itself and provides an easy default implementation
69 | // for embedding in a Node. Embedded in all non-trivial Nodes.
70 | func (t NodeType) Type() NodeType {
71 | return t
72 | }
73 |
74 | const (
75 | NodeText NodeType = iota //Plain text.
76 | NodeAction //A non-control action such as a field evaluation.
77 | NodeChain //A sequence of field accesses.
78 | NodeCommand //An element of a pipeline.
79 | NodeField //A field or method name.
80 | NodeIdentifier //An identifier; always a function name.
81 | NodeUnderscore //An underscore (discard in assignment, or slot in argument list for piped value)
82 | NodeList //A list of Nodes.
83 | NodePipe //A pipeline of commands.
84 | NodeSet
85 | //NodeWith //A with action.
86 | NodeInclude
87 | NodeBlock
88 | nodeEnd //An end action. Not added to tree.
89 | NodeYield
90 | nodeContent
91 | NodeIf //An if action.
92 | nodeElse //An else action. Not added to tree.
93 | NodeRange //A range action.
94 | NodeTry
95 | nodeCatch
96 | NodeReturn
97 | beginExpressions
98 | NodeString //A string constant.
99 | NodeNil //An untyped nil constant.
100 | NodeNumber //A numerical constant.
101 | NodeBool //A boolean constant.
102 | NodeAdditiveExpr
103 | NodeMultiplicativeExpr
104 | NodeComparativeExpr
105 | NodeNumericComparativeExpr
106 | NodeLogicalExpr
107 | NodeCallExpr
108 | NodeNotExpr
109 | NodeTernaryExpr
110 | NodeIndexExpr
111 | NodeSliceExpr
112 | endExpressions
113 | )
114 |
115 | // Nodes.
116 |
117 | // ListNode holds a sequence of nodes.
118 | type ListNode struct {
119 | NodeBase
120 | Nodes []Node //The element nodes in lexical order.
121 | }
122 |
123 | func (l *ListNode) append(n Node) {
124 | l.Nodes = append(l.Nodes, n)
125 | }
126 |
127 | func (l *ListNode) String() string {
128 | b := new(bytes.Buffer)
129 | for _, n := range l.Nodes {
130 | fmt.Fprint(b, n)
131 | }
132 | return b.String()
133 | }
134 |
135 | // TextNode holds plain text.
136 | type TextNode struct {
137 | NodeBase
138 | Text []byte
139 | }
140 |
141 | func (t *TextNode) String() string {
142 | return fmt.Sprintf(textFormat, t.Text)
143 | }
144 |
145 | // PipeNode holds a pipeline with optional declaration
146 | type PipeNode struct {
147 | NodeBase //The line number in the input. Deprecated: Kept for compatibility.
148 | Cmds []*CommandNode //The commands in lexical order.
149 | }
150 |
151 | func (p *PipeNode) append(command *CommandNode) {
152 | p.Cmds = append(p.Cmds, command)
153 | }
154 |
155 | func (p *PipeNode) String() string {
156 | s := ""
157 | for i, c := range p.Cmds {
158 | if i > 0 {
159 | s += " | "
160 | }
161 | s += c.String()
162 | }
163 | return s
164 | }
165 |
166 | // ActionNode holds an action (something bounded by delimiters).
167 | // Control actions have their own nodes; ActionNode represents simple
168 | // ones such as field evaluations and parenthesized pipelines.
169 | type ActionNode struct {
170 | NodeBase
171 | Set *SetNode
172 | Pipe *PipeNode
173 | }
174 |
175 | func (a *ActionNode) String() string {
176 | if a.Set != nil {
177 | if a.Pipe == nil {
178 | return fmt.Sprintf("{{%s}}", a.Set)
179 | }
180 | return fmt.Sprintf("{{%s;%s}}", a.Set, a.Pipe)
181 | }
182 | return fmt.Sprintf("{{%s}}", a.Pipe)
183 | }
184 |
185 | // CommandNode holds a command (a pipeline inside an evaluating action).
186 | type CommandNode struct {
187 | NodeBase
188 | CallExprNode
189 | }
190 |
191 | func (c *CommandNode) append(arg Node) {
192 | c.Exprs = append(c.Exprs, arg)
193 | }
194 |
195 | func (c *CommandNode) String() string {
196 | if c.Exprs == nil {
197 | return c.BaseExpr.String()
198 | }
199 |
200 | arguments := ""
201 | for i, expr := range c.Exprs {
202 | if i > 0 {
203 | arguments += ", "
204 | }
205 | arguments += expr.String()
206 | }
207 | return fmt.Sprintf("%s(%s)", c.BaseExpr, arguments)
208 | }
209 |
210 | // IdentifierNode holds an identifier.
211 | type IdentifierNode struct {
212 | NodeBase
213 | Ident string //The identifier's name.
214 | }
215 |
216 | func (i *IdentifierNode) String() string {
217 | return i.Ident
218 | }
219 |
220 | // UnderscoreNode is used for one of two things:
221 | // - signals to discard the corresponding right side of an assignment
222 | // - tells Jet where in a pipelined function call to inject the piped value
223 | type UnderscoreNode struct {
224 | NodeBase
225 | }
226 |
227 | func (i *UnderscoreNode) String() string {
228 | return "_"
229 | }
230 |
231 | // NilNode holds the special identifier 'nil' representing an untyped nil constant.
232 | type NilNode struct {
233 | NodeBase
234 | }
235 |
236 | func (n *NilNode) String() string {
237 | return "nil"
238 | }
239 |
240 | // FieldNode holds a field (identifier starting with '.').
241 | // The names may be chained ('.x.y').
242 | // The period is dropped from each ident.
243 | type FieldNode struct {
244 | NodeBase
245 | Ident []string //The identifiers in lexical order.
246 | }
247 |
248 | func (f *FieldNode) String() string {
249 | s := ""
250 | for _, id := range f.Ident {
251 | s += "." + id
252 | }
253 | return s
254 | }
255 |
256 | // ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
257 | // The names may be chained ('.x.y').
258 | // The periods are dropped from each ident.
259 | type ChainNode struct {
260 | NodeBase
261 | Node Node
262 | Field []string //The identifiers in lexical order.
263 | }
264 |
265 | // Add adds the named field (which should start with a period) to the end of the chain.
266 | func (c *ChainNode) Add(field string) {
267 | if len(field) == 0 || field[0] != '.' {
268 | panic("no dot in field")
269 | }
270 | field = field[1:] //Remove leading dot.
271 | if field == "" {
272 | panic("empty field")
273 | }
274 | c.Field = append(c.Field, field)
275 | }
276 |
277 | func (c *ChainNode) String() string {
278 | s := c.Node.String()
279 | if _, ok := c.Node.(*PipeNode); ok {
280 | s = "(" + s + ")"
281 | }
282 | for _, field := range c.Field {
283 | s += "." + field
284 | }
285 | return s
286 | }
287 |
288 | // BoolNode holds a boolean constant.
289 | type BoolNode struct {
290 | NodeBase
291 | True bool //The value of the boolean constant.
292 | }
293 |
294 | func (b *BoolNode) String() string {
295 | if b.True {
296 | return "true"
297 | }
298 | return "false"
299 | }
300 |
301 | // NumberNode holds a number: signed or unsigned integer, float, or complex.
302 | // The value is parsed and stored under all the types that can represent the value.
303 | // This simulates in a small amount of code the behavior of Go's ideal constants.
304 | type NumberNode struct {
305 | NodeBase
306 |
307 | IsInt bool //Number has an integral value.
308 | IsUint bool //Number has an unsigned integral value.
309 | IsFloat bool //Number has a floating-point value.
310 | IsComplex bool //Number is complex.
311 | Int64 int64 //The signed integer value.
312 | Uint64 uint64 //The unsigned integer value.
313 | Float64 float64 //The floating-point value.
314 | Complex128 complex128 //The complex value.
315 | Text string //The original textual representation from the input.
316 | }
317 |
318 | // simplifyComplex pulls out any other types that are represented by the complex number.
319 | // These all require that the imaginary part be zero.
320 | func (n *NumberNode) simplifyComplex() {
321 | n.IsFloat = imag(n.Complex128) == 0
322 | if n.IsFloat {
323 | n.Float64 = real(n.Complex128)
324 | n.IsInt = float64(int64(n.Float64)) == n.Float64
325 | if n.IsInt {
326 | n.Int64 = int64(n.Float64)
327 | }
328 | n.IsUint = float64(uint64(n.Float64)) == n.Float64
329 | if n.IsUint {
330 | n.Uint64 = uint64(n.Float64)
331 | }
332 | }
333 | }
334 |
335 | func (n *NumberNode) String() string {
336 | return n.Text
337 | }
338 |
339 | // StringNode holds a string constant. The value has been "unquoted".
340 | type StringNode struct {
341 | NodeBase
342 |
343 | Quoted string //The original text of the string, with quotes.
344 | Text string //The string, after quote processing.
345 | }
346 |
347 | func (s *StringNode) String() string {
348 | return s.Quoted
349 | }
350 |
351 | // endNode represents an {{end}} action.
352 | // It does not appear in the final parse tree.
353 | type endNode struct {
354 | NodeBase
355 | }
356 |
357 | func (e *endNode) String() string {
358 | return "{{end}}"
359 | }
360 |
361 | // endNode represents an {{end}} action.
362 | // It does not appear in the final parse tree.
363 | type contentNode struct {
364 | NodeBase
365 | }
366 |
367 | func (e *contentNode) String() string {
368 | return "{{content}}"
369 | }
370 |
371 | // elseNode represents an {{else}} action. Does not appear in the final tree.
372 | type elseNode struct {
373 | NodeBase //The line number in the input. Deprecated: Kept for compatibility.
374 | }
375 |
376 | func (e *elseNode) String() string {
377 | return "{{else}}"
378 | }
379 |
380 | // SetNode represents a set action, ident( ',' ident)* '=' expression ( ',' expression )*
381 | type SetNode struct {
382 | NodeBase
383 | Let bool
384 | IndexExprGetLookup bool
385 | Left []Expression
386 | Right []Expression
387 | }
388 |
389 | func (set *SetNode) String() string {
390 | var s = ""
391 |
392 | for i, v := range set.Left {
393 | if i > 0 {
394 | s += ", "
395 | }
396 | s += v.String()
397 | }
398 |
399 | if set.Let {
400 | s += ":="
401 | } else {
402 | s += "="
403 | }
404 |
405 | for i, v := range set.Right {
406 | if i > 0 {
407 | s += ", "
408 | }
409 | s += v.String()
410 | }
411 |
412 | return s
413 | }
414 |
415 | // BranchNode is the common representation of if, range, and with.
416 | type BranchNode struct {
417 | NodeBase
418 | Set *SetNode
419 | Expression Expression
420 | List *ListNode
421 | ElseList *ListNode
422 | }
423 |
424 | func (b *BranchNode) String() string {
425 |
426 | if b.NodeType == NodeRange {
427 | s := ""
428 | if b.Set != nil {
429 | s = b.Set.String()
430 | } else {
431 | s = b.Expression.String()
432 | }
433 |
434 | if b.ElseList != nil {
435 | return fmt.Sprintf("{{range %s}}%s{{else}}%s{{end}}", s, b.List, b.ElseList)
436 | }
437 | return fmt.Sprintf("{{range %s}}%s{{end}}", s, b.List)
438 | } else {
439 | s := ""
440 | if b.Set != nil {
441 | s = b.Set.String() + ";"
442 | }
443 | if b.ElseList != nil {
444 | return fmt.Sprintf("{{if %s%s}}%s{{else}}%s{{end}}", s, b.Expression, b.List, b.ElseList)
445 | }
446 | return fmt.Sprintf("{{if %s%s}}%s{{end}}", s, b.Expression, b.List)
447 | }
448 | }
449 |
450 | // IfNode represents an {{if}} action and its commands.
451 | type IfNode struct {
452 | BranchNode
453 | }
454 |
455 | // RangeNode represents a {{range}} action and its commands.
456 | type RangeNode struct {
457 | BranchNode
458 | }
459 |
460 | type BlockParameter struct {
461 | Identifier string
462 | Expression Expression
463 | }
464 |
465 | type BlockParameterList struct {
466 | NodeBase
467 | List []BlockParameter
468 | }
469 |
470 | func (bplist *BlockParameterList) Param(name string) (Expression, int) {
471 | for i := 0; i < len(bplist.List); i++ {
472 | param := &bplist.List[i]
473 | if param.Identifier == name {
474 | return param.Expression, i
475 | }
476 | }
477 | return nil, -1
478 | }
479 |
480 | func (bplist *BlockParameterList) String() (str string) {
481 | buff := bytes.NewBuffer(nil)
482 | for _, bp := range bplist.List {
483 | if bp.Identifier == "" {
484 | fmt.Fprintf(buff, "%s,", bp.Expression)
485 | } else {
486 | if bp.Expression == nil {
487 | fmt.Fprintf(buff, "%s,", bp.Identifier)
488 | } else {
489 | fmt.Fprintf(buff, "%s=%s,", bp.Identifier, bp.Expression)
490 | }
491 | }
492 | }
493 | if buff.Len() > 0 {
494 | str = buff.String()[0 : buff.Len()-1]
495 | }
496 | return
497 | }
498 |
499 | // BlockNode represents a {{block }} action.
500 | type BlockNode struct {
501 | NodeBase //The line number in the input. Deprecated: Kept for compatibility.
502 | Name string //The name of the template (unquoted).
503 |
504 | Parameters *BlockParameterList
505 | Expression Expression //The command to evaluate as dot for the template.
506 |
507 | List *ListNode
508 | Content *ListNode
509 | }
510 |
511 | func (t *BlockNode) String() string {
512 | if t.Content != nil {
513 | if t.Expression == nil {
514 | return fmt.Sprintf("{{block %s(%s)}}%s{{content}}%s{{end}}", t.Name, t.Parameters, t.List, t.Content)
515 | }
516 | return fmt.Sprintf("{{block %s(%s) %s}}%s{{content}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.List, t.Content)
517 | }
518 | if t.Expression == nil {
519 | return fmt.Sprintf("{{block %s(%s)}}%s{{end}}", t.Name, t.Parameters, t.List)
520 | }
521 | return fmt.Sprintf("{{block %s(%s) %s}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.List)
522 | }
523 |
524 | // YieldNode represents a {{yield}} action
525 | type YieldNode struct {
526 | NodeBase //The line number in the input. Deprecated: Kept for compatibility.
527 | Name string //The name of the template (unquoted).
528 | Parameters *BlockParameterList
529 | Expression Expression //The command to evaluate as dot for the template.
530 | Content *ListNode
531 | IsContent bool
532 | }
533 |
534 | func (t *YieldNode) String() string {
535 | if t.IsContent {
536 | if t.Expression == nil {
537 | return "{{yield content}}"
538 | }
539 | return fmt.Sprintf("{{yield content %s}}", t.Expression)
540 | }
541 |
542 | if t.Content != nil {
543 | if t.Expression == nil {
544 | return fmt.Sprintf("{{yield %s(%s) content}}%s{{end}}", t.Name, t.Parameters, t.Content)
545 | }
546 | return fmt.Sprintf("{{yield %s(%s) %s content}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.Content)
547 | }
548 |
549 | if t.Expression == nil {
550 | return fmt.Sprintf("{{yield %s(%s)}}", t.Name, t.Parameters)
551 | }
552 | return fmt.Sprintf("{{yield %s(%s) %s}}", t.Name, t.Parameters, t.Expression)
553 | }
554 |
555 | // IncludeNode represents a {{include }} action.
556 | type IncludeNode struct {
557 | NodeBase
558 | Name Expression
559 | Context Expression
560 | }
561 |
562 | func (t *IncludeNode) String() string {
563 | if t.Context == nil {
564 | return fmt.Sprintf("{{include %s}}", t.Name)
565 | }
566 | return fmt.Sprintf("{{include %s %s}}", t.Name, t.Context)
567 | }
568 |
569 | type binaryExprNode struct {
570 | NodeBase
571 | Operator item
572 | Left, Right Expression
573 | }
574 |
575 | func (node *binaryExprNode) String() string {
576 | return fmt.Sprintf("%s %s %s", node.Left, node.Operator.val, node.Right)
577 | }
578 |
579 | // AdditiveExprNode represents an add or subtract expression
580 | // ex: expression ( '+' | '-' ) expression
581 | type AdditiveExprNode struct {
582 | binaryExprNode
583 | }
584 |
585 | // MultiplicativeExprNode represents a multiplication, division, or module expression
586 | // ex: expression ( '*' | '/' | '%' ) expression
587 | type MultiplicativeExprNode struct {
588 | binaryExprNode
589 | }
590 |
591 | // LogicalExprNode represents a boolean expression, 'and' or 'or'
592 | // ex: expression ( '&&' | '||' ) expression
593 | type LogicalExprNode struct {
594 | binaryExprNode
595 | }
596 |
597 | // ComparativeExprNode represents a comparative expression
598 | // ex: expression ( '==' | '!=' ) expression
599 | type ComparativeExprNode struct {
600 | binaryExprNode
601 | }
602 |
603 | // NumericComparativeExprNode represents a numeric comparative expression
604 | // ex: expression ( '<' | '>' | '<=' | '>=' ) expression
605 | type NumericComparativeExprNode struct {
606 | binaryExprNode
607 | }
608 |
609 | // NotExprNode represents a negate expression
610 | // ex: '!' expression
611 | type NotExprNode struct {
612 | NodeBase
613 | Expr Expression
614 | }
615 |
616 | func (s *NotExprNode) String() string {
617 | return fmt.Sprintf("!%s", s.Expr)
618 | }
619 |
620 | type CallArgs struct {
621 | Exprs []Expression
622 | HasPipeSlot bool
623 | }
624 |
625 | // CallExprNode represents a call expression
626 | // ex: expression '(' (expression (',' expression)* )? ')'
627 | type CallExprNode struct {
628 | NodeBase
629 | BaseExpr Expression
630 | CallArgs
631 | }
632 |
633 | func (s *CallExprNode) String() string {
634 | arguments := ""
635 | for i, expr := range s.Exprs {
636 | if i > 0 {
637 | arguments += ", "
638 | }
639 | arguments += expr.String()
640 | }
641 | return fmt.Sprintf("%s(%s)", s.BaseExpr, arguments)
642 | }
643 |
644 | // TernaryExprNod represents a ternary expression,
645 | // ex: expression '?' expression ':' expression
646 | type TernaryExprNode struct {
647 | NodeBase
648 | Boolean, Left, Right Expression
649 | }
650 |
651 | func (s *TernaryExprNode) String() string {
652 | return fmt.Sprintf("%s?%s:%s", s.Boolean, s.Left, s.Right)
653 | }
654 |
655 | type IndexExprNode struct {
656 | NodeBase
657 | Base Expression
658 | Index Expression
659 | }
660 |
661 | func (s *IndexExprNode) String() string {
662 | return fmt.Sprintf("%s[%s]", s.Base, s.Index)
663 | }
664 |
665 | type SliceExprNode struct {
666 | NodeBase
667 | Base Expression
668 | Index Expression
669 | EndIndex Expression
670 | }
671 |
672 | func (s *SliceExprNode) String() string {
673 | var index_string, len_string string
674 | if s.Index != nil {
675 | index_string = s.Index.String()
676 | }
677 | if s.EndIndex != nil {
678 | len_string = s.EndIndex.String()
679 | }
680 | return fmt.Sprintf("%s[%s:%s]", s.Base, index_string, len_string)
681 | }
682 |
683 | type ReturnNode struct {
684 | NodeBase
685 | Value Expression
686 | }
687 |
688 | func (n *ReturnNode) String() string {
689 | return fmt.Sprintf("return %v", n.Value)
690 | }
691 |
692 | type TryNode struct {
693 | NodeBase
694 | List *ListNode
695 | Catch *catchNode
696 | }
697 |
698 | func (n *TryNode) String() string {
699 | if n.Catch != nil {
700 | return fmt.Sprintf("{{try}}%s%s", n.List, n.Catch)
701 | }
702 | return fmt.Sprintf("{{try}}%s{{end}}", n.List)
703 | }
704 |
705 | type catchNode struct {
706 | NodeBase
707 | Err *IdentifierNode
708 | List *ListNode
709 | }
710 |
711 | func (n *catchNode) String() string {
712 | return fmt.Sprintf("{{catch %s}}%s{{end}}", n.Err, n.List)
713 | }
714 |
--------------------------------------------------------------------------------
/lex.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 José Santos
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jet
16 |
17 | import (
18 | "fmt"
19 | "strings"
20 | "unicode"
21 | "unicode/utf8"
22 | )
23 |
24 | // item represents a token or text string returned from the scanner.
25 | type item struct {
26 | typ itemType // The type of this item.
27 | pos Pos // The starting position, in bytes, of this item in the input string.
28 | val string // The value of this item.
29 | }
30 |
31 | func (i item) String() string {
32 | switch {
33 | case i.typ == itemEOF:
34 | return "EOF"
35 | case i.typ == itemError:
36 | return i.val
37 | case i.typ > itemKeyword:
38 | return fmt.Sprintf("<%s>", i.val)
39 | case len(i.val) > 10:
40 | return fmt.Sprintf("%.10q...", i.val)
41 | }
42 | return fmt.Sprintf("%q", i.val)
43 | }
44 |
45 | // itemType identifies the type of lex items.
46 | type itemType int
47 |
48 | const (
49 | itemError itemType = iota // error occurred; value is text of error
50 | itemBool // boolean constant
51 | itemChar // printable ASCII character; grab bag for comma etc.
52 | itemCharConstant // character constant
53 | itemComplex // complex constant (1+2i); imaginary is just a number
54 | itemEOF
55 | itemField // alphanumeric identifier starting with '.'
56 | itemIdentifier // alphanumeric identifier not starting with '.'
57 | itemLeftDelim // left action delimiter
58 | itemLeftParen // '(' inside action
59 | itemNumber // simple number, including imaginary
60 | itemPipe // pipe symbol
61 | itemRawString // raw quoted string (includes quotes)
62 | itemRightDelim // right action delimiter
63 | itemRightParen // ')' inside action
64 | itemSpace // run of spaces separating arguments
65 | itemString // quoted string (includes quotes)
66 | itemText // plain text
67 | itemAssign
68 | itemEquals
69 | itemNotEquals
70 | itemGreat
71 | itemGreatEquals
72 | itemLess
73 | itemLessEquals
74 | itemComma
75 | itemSemicolon
76 | itemAdd
77 | itemMinus
78 | itemMul
79 | itemDiv
80 | itemMod
81 | itemColon
82 | itemTernary
83 | itemLeftBrackets
84 | itemRightBrackets
85 | itemUnderscore
86 | // Keywords appear after all the rest.
87 | itemKeyword // used only to delimit the keywords
88 | itemExtends
89 | itemImport
90 | itemInclude
91 | itemBlock
92 | itemEnd
93 | itemYield
94 | itemContent
95 | itemIf
96 | itemElse
97 | itemRange
98 | itemTry
99 | itemCatch
100 | itemReturn
101 | itemAnd
102 | itemOr
103 | itemNot
104 | itemNil
105 | itemMSG
106 | itemTrans
107 | )
108 |
109 | var key = map[string]itemType{
110 | "extends": itemExtends,
111 | "import": itemImport,
112 |
113 | "include": itemInclude,
114 | "block": itemBlock,
115 | "end": itemEnd,
116 | "yield": itemYield,
117 | "content": itemContent,
118 |
119 | "if": itemIf,
120 | "else": itemElse,
121 |
122 | "range": itemRange,
123 |
124 | "try": itemTry,
125 | "catch": itemCatch,
126 |
127 | "return": itemReturn,
128 |
129 | "and": itemAnd,
130 | "or": itemOr,
131 | "not": itemNot,
132 |
133 | "nil": itemNil,
134 |
135 | "msg": itemMSG,
136 | "trans": itemTrans,
137 | }
138 |
139 | const eof = -1
140 |
141 | const (
142 | defaultLeftDelim = "{{"
143 | defaultRightDelim = "}}"
144 | defaultLeftComment = "{*"
145 | defaultRightComment = "*}"
146 | leftTrimMarker = "- "
147 | rightTrimMarker = " -"
148 | trimMarkerLen = Pos(len(leftTrimMarker))
149 | )
150 |
151 | // stateFn represents the state of the scanner as a function that returns the next state.
152 | type stateFn func(*lexer) stateFn
153 |
154 | // lexer holds the state of the scanner.
155 | type lexer struct {
156 | name string // the name of the input; used only for error reports
157 | input string // the string being scanned
158 | state stateFn // the next lexing function to enter
159 | pos Pos // current position in the input
160 | start Pos // start position of this item
161 | width Pos // width of last rune read from input
162 | lastPos Pos // position of most recent item returned by nextItem
163 | items chan item // channel of scanned items
164 | parenDepth int // nesting depth of ( ) exprs
165 | lastType itemType
166 | leftDelim string
167 | rightDelim string
168 | leftComment string
169 | rightComment string
170 | trimRightDelim string
171 | }
172 |
173 | func (l *lexer) setDelimiters(leftDelim, rightDelim string) {
174 | if leftDelim != "" {
175 | l.leftDelim = leftDelim
176 | }
177 | if rightDelim != "" {
178 | l.rightDelim = rightDelim
179 | }
180 | }
181 |
182 | func (l *lexer) setCommentDelimiters(leftDelim, rightDelim string) {
183 | if leftDelim != "" {
184 | l.leftComment = leftDelim
185 | }
186 | if rightDelim != "" {
187 | l.rightComment = rightDelim
188 | }
189 | }
190 |
191 | // next returns the next rune in the input.
192 | func (l *lexer) next() rune {
193 | if int(l.pos) >= len(l.input) {
194 | l.width = 0
195 | return eof
196 | }
197 | r, w := utf8.DecodeRuneInString(l.input[l.pos:])
198 | l.width = Pos(w)
199 | l.pos += l.width
200 | return r
201 | }
202 |
203 | // peek returns but does not consume the next rune in the input.
204 | func (l *lexer) peek() rune {
205 | r := l.next()
206 | l.backup()
207 | return r
208 | }
209 |
210 | // backup steps back one rune. Can only be called once per call of next.
211 | func (l *lexer) backup() {
212 | l.pos -= l.width
213 | }
214 |
215 | // emit passes an item back to the client.
216 | func (l *lexer) emit(t itemType) {
217 | l.lastType = t
218 | l.items <- item{t, l.start, l.input[l.start:l.pos]}
219 | l.start = l.pos
220 | }
221 |
222 | // ignore skips over the pending input before this point.
223 | func (l *lexer) ignore() {
224 | l.start = l.pos
225 | }
226 |
227 | // accept consumes the next rune if it's from the valid set.
228 | func (l *lexer) accept(valid string) bool {
229 | if strings.IndexRune(valid, l.next()) >= 0 {
230 | return true
231 | }
232 | l.backup()
233 | return false
234 | }
235 |
236 | // acceptRun consumes a run of runes from the valid set.
237 | func (l *lexer) acceptRun(valid string) {
238 | for strings.IndexRune(valid, l.next()) >= 0 {
239 | }
240 | l.backup()
241 | }
242 |
243 | // lineNumber reports which line we're on, based on the position of
244 | // the previous item returned by nextItem. Doing it this way
245 | // means we don't have to worry about peek double counting.
246 | func (l *lexer) lineNumber() int {
247 | return 1 + strings.Count(l.input[:l.lastPos], "\n")
248 | }
249 |
250 | // errorf returns an error token and terminates the scan by passing
251 | // back a nil pointer that will be the next state, terminating l.nextItem.
252 | func (l *lexer) errorf(format string, args ...interface{}) stateFn {
253 | l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}
254 | return nil
255 | }
256 |
257 | // nextItem returns the next item from the input.
258 | // Called by the parser, not in the lexing goroutine.
259 | func (l *lexer) nextItem() item {
260 | item := <-l.items
261 | l.lastPos = item.pos
262 | return item
263 | }
264 |
265 | // drain drains the output so the lexing goroutine will exit.
266 | // Called by the parser, not in the lexing goroutine.
267 | func (l *lexer) drain() {
268 | for range l.items {
269 | }
270 | }
271 |
272 | // lex creates a new scanner for the input string.
273 | func lex(name, input string, run bool) *lexer {
274 | l := &lexer{
275 | name: name,
276 | input: input,
277 | items: make(chan item),
278 | leftDelim: defaultLeftDelim,
279 | rightDelim: defaultRightDelim,
280 | leftComment: defaultLeftComment,
281 | rightComment: defaultRightComment,
282 | trimRightDelim: rightTrimMarker + defaultRightDelim,
283 | }
284 | if run {
285 | l.run()
286 | }
287 | return l
288 | }
289 |
290 | // run runs the state machine for the lexer.
291 | func (l *lexer) run() {
292 | go func() {
293 | for l.state = lexText; l.state != nil; {
294 | l.state = l.state(l)
295 | }
296 | close(l.items)
297 | }()
298 | }
299 |
300 | // state functions
301 | func lexText(l *lexer) stateFn {
302 | for {
303 | // without breaking the API, this seems like a reasonable workaround to correctly parse comments
304 | i := strings.IndexByte(l.input[l.pos:], l.leftDelim[0]) // index of suspected left delimiter
305 | ic := strings.IndexByte(l.input[l.pos:], l.leftComment[0]) // index of suspected left comment marker
306 | if ic > -1 && ic < i { // use whichever is lower for future lexing
307 | i = ic
308 | }
309 | // if no token is found, skip till the end of template
310 | if i == -1 {
311 | l.pos = Pos(len(l.input))
312 | break
313 | } else {
314 | l.pos += Pos(i)
315 | if strings.HasPrefix(l.input[l.pos:], l.leftDelim) {
316 | ld := Pos(len(l.leftDelim))
317 | trimLength := Pos(0)
318 | if strings.HasPrefix(l.input[l.pos+ld:], leftTrimMarker) {
319 | trimLength = rightTrimLength(l.input[l.start:l.pos])
320 | }
321 | l.pos -= trimLength
322 | if l.pos > l.start {
323 | l.emit(itemText)
324 | }
325 | l.pos += trimLength
326 | l.ignore()
327 | return lexLeftDelim
328 | }
329 | if strings.HasPrefix(l.input[l.pos:], l.leftComment) {
330 | if l.pos > l.start {
331 | l.emit(itemText)
332 | }
333 | return lexComment
334 | }
335 | }
336 | if l.next() == eof {
337 | break
338 | }
339 | }
340 | // Correctly reached EOF.
341 | if l.pos > l.start {
342 | l.emit(itemText)
343 | }
344 | l.emit(itemEOF)
345 | return nil
346 | }
347 |
348 | func lexLeftDelim(l *lexer) stateFn {
349 | l.pos += Pos(len(l.leftDelim))
350 | l.emit(itemLeftDelim)
351 | trimSpace := strings.HasPrefix(l.input[l.pos:], leftTrimMarker)
352 | if trimSpace {
353 | l.pos += trimMarkerLen
354 | l.ignore()
355 | }
356 | l.parenDepth = 0
357 | return lexInsideAction
358 | }
359 |
360 | // lexComment scans a comment. The left comment marker is known to be present.
361 | func lexComment(l *lexer) stateFn {
362 | l.pos += Pos(len(l.leftComment))
363 | i := strings.Index(l.input[l.pos:], l.rightComment)
364 | if i < 0 {
365 | return l.errorf("unclosed comment")
366 | }
367 | l.pos += Pos(i + len(l.rightComment))
368 | l.ignore()
369 | return lexText
370 | }
371 |
372 | // lexRightDelim scans the right delimiter, which is known to be present.
373 | func lexRightDelim(l *lexer) stateFn {
374 | trimSpace := strings.HasPrefix(l.input[l.pos:], rightTrimMarker)
375 | if trimSpace {
376 | l.pos += trimMarkerLen
377 | l.ignore()
378 | }
379 | l.pos += Pos(len(l.rightDelim))
380 | l.emit(itemRightDelim)
381 | if trimSpace {
382 | l.pos += leftTrimLength(l.input[l.pos:])
383 | l.ignore()
384 | }
385 | return lexText
386 | }
387 |
388 | // lexInsideAction scans the elements inside action delimiters.
389 | func lexInsideAction(l *lexer) stateFn {
390 | // Either number, quoted string, or identifier.
391 | // Spaces separate arguments; runs of spaces turn into itemSpace.
392 | // Pipe symbols separate and are emitted.
393 | delim, _ := l.atRightDelim()
394 | if delim {
395 | if l.parenDepth == 0 {
396 | return lexRightDelim
397 | }
398 | return l.errorf("unclosed left parenthesis")
399 | }
400 | switch r := l.next(); {
401 | case r == eof:
402 | return l.errorf("unclosed action")
403 | case isSpace(r):
404 | return lexSpace
405 | case r == ',':
406 | l.emit(itemComma)
407 | case r == ';':
408 | l.emit(itemSemicolon)
409 | case r == '*':
410 | l.emit(itemMul)
411 | case r == '/':
412 | l.emit(itemDiv)
413 | case r == '%':
414 | l.emit(itemMod)
415 | case r == '-':
416 |
417 | if r := l.peek(); '0' <= r && r <= '9' &&
418 | itemAdd != l.lastType &&
419 | itemMinus != l.lastType &&
420 | itemNumber != l.lastType &&
421 | itemIdentifier != l.lastType &&
422 | itemString != l.lastType &&
423 | itemRawString != l.lastType &&
424 | itemCharConstant != l.lastType &&
425 | itemBool != l.lastType &&
426 | itemField != l.lastType &&
427 | itemChar != l.lastType &&
428 | itemTrans != l.lastType {
429 | l.backup()
430 | return lexNumber
431 | }
432 | l.emit(itemMinus)
433 | case r == '+':
434 | if r := l.peek(); '0' <= r && r <= '9' &&
435 | itemAdd != l.lastType &&
436 | itemMinus != l.lastType &&
437 | itemNumber != l.lastType &&
438 | itemIdentifier != l.lastType &&
439 | itemString != l.lastType &&
440 | itemRawString != l.lastType &&
441 | itemCharConstant != l.lastType &&
442 | itemBool != l.lastType &&
443 | itemField != l.lastType &&
444 | itemChar != l.lastType &&
445 | itemTrans != l.lastType {
446 | l.backup()
447 | return lexNumber
448 | }
449 | l.emit(itemAdd)
450 | case r == '?':
451 | l.emit(itemTernary)
452 | case r == '&':
453 | if l.next() == '&' {
454 | l.emit(itemAnd)
455 | } else {
456 | l.backup()
457 | }
458 | case r == '<':
459 | if l.next() == '=' {
460 | l.emit(itemLessEquals)
461 | } else {
462 | l.backup()
463 | l.emit(itemLess)
464 | }
465 | case r == '>':
466 | if l.next() == '=' {
467 | l.emit(itemGreatEquals)
468 | } else {
469 | l.backup()
470 | l.emit(itemGreat)
471 | }
472 | case r == '!':
473 | if l.next() == '=' {
474 | l.emit(itemNotEquals)
475 | } else {
476 | l.backup()
477 | l.emit(itemNot)
478 | }
479 |
480 | case r == '=':
481 | if l.next() == '=' {
482 | l.emit(itemEquals)
483 | } else {
484 | l.backup()
485 | l.emit(itemAssign)
486 | }
487 | case r == ':':
488 | if l.next() == '=' {
489 | l.emit(itemAssign)
490 | } else {
491 | l.backup()
492 | l.emit(itemColon)
493 | }
494 | case r == '|':
495 | if l.next() == '|' {
496 | l.emit(itemOr)
497 | } else {
498 | l.backup()
499 | l.emit(itemPipe)
500 | }
501 | case r == '"':
502 | return lexQuote
503 | case r == '`':
504 | return lexRawQuote
505 | case r == '\'':
506 | return lexChar
507 | case r == '.':
508 | // special look-ahead for ".field" so we don't break l.backup().
509 | if l.pos < Pos(len(l.input)) {
510 | r := l.input[l.pos]
511 | if r < '0' || '9' < r {
512 | return lexField
513 | }
514 | }
515 | fallthrough // '.' can start a number.
516 | case '0' <= r && r <= '9':
517 | l.backup()
518 | return lexNumber
519 | case r == '_':
520 | if !isAlphaNumeric(l.peek()) {
521 | l.emit(itemUnderscore)
522 | return lexInsideAction
523 | }
524 | fallthrough // no space? must be the start of an identifier
525 | case isAlphaNumeric(r):
526 | l.backup()
527 | return lexIdentifier
528 | case r == '[':
529 | l.emit(itemLeftBrackets)
530 | case r == ']':
531 | l.emit(itemRightBrackets)
532 | case r == '(':
533 | l.emit(itemLeftParen)
534 | l.parenDepth++
535 | case r == ')':
536 | l.emit(itemRightParen)
537 | l.parenDepth--
538 | if l.parenDepth < 0 {
539 | return l.errorf("unexpected right paren %#U", r)
540 | }
541 | case r <= unicode.MaxASCII && unicode.IsPrint(r):
542 | l.emit(itemChar)
543 | default:
544 | return l.errorf("unrecognized character in action: %#U", r)
545 | }
546 | return lexInsideAction
547 | }
548 |
549 | // lexSpace scans a run of space characters.
550 | // One space has already been seen.
551 | func lexSpace(l *lexer) stateFn {
552 | var numSpaces int
553 | for isSpace(l.peek()) {
554 | numSpaces++
555 | l.next()
556 | }
557 | if strings.HasPrefix(l.input[l.pos-1:], l.trimRightDelim) {
558 | l.backup()
559 | if numSpaces == 1 {
560 | return lexRightDelim
561 | }
562 | }
563 | l.emit(itemSpace)
564 | return lexInsideAction
565 | }
566 |
567 | // lexIdentifier scans an alphanumeric.
568 | func lexIdentifier(l *lexer) stateFn {
569 | Loop:
570 | for {
571 | switch r := l.next(); {
572 | case isAlphaNumeric(r):
573 | // absorb.
574 | default:
575 | l.backup()
576 | word := l.input[l.start:l.pos]
577 | if !l.atTerminator() {
578 | return l.errorf("bad character %#U", r)
579 | }
580 | switch {
581 | case key[word] > itemKeyword:
582 | l.emit(key[word])
583 | case word[0] == '.':
584 | l.emit(itemField)
585 | case word == "true", word == "false":
586 | l.emit(itemBool)
587 | default:
588 | l.emit(itemIdentifier)
589 | }
590 | break Loop
591 | }
592 | }
593 | return lexInsideAction
594 | }
595 |
596 | // lexField scans a field: .Alphanumeric.
597 | // The . has been scanned.
598 | func lexField(l *lexer) stateFn {
599 |
600 | if l.atTerminator() {
601 | // Nothing interesting follows -> "." or "$".
602 | l.emit(itemIdentifier)
603 | return lexInsideAction
604 | }
605 |
606 | var r rune
607 | for {
608 | r = l.next()
609 | if !isAlphaNumeric(r) {
610 | l.backup()
611 | break
612 | }
613 | }
614 | if !l.atTerminator() {
615 | return l.errorf("bad character %#U", r)
616 | }
617 | l.emit(itemField)
618 | return lexInsideAction
619 | }
620 |
621 | // atTerminator reports whether the input is at valid termination character to
622 | // appear after an identifier. Breaks .X.Y into two pieces. Also catches cases
623 | // like "$x+2" not being acceptable without a space, in case we decide one
624 | // day to implement arithmetic.
625 | func (l *lexer) atTerminator() bool {
626 | r := l.peek()
627 | if isSpace(r) {
628 | return true
629 | }
630 | switch r {
631 | case eof, '.', ',', '|', ':', ')', '=', '(', ';', '?', '[', ']', '+', '-', '/', '%', '*', '&', '!', '<', '>':
632 | return true
633 | }
634 | // Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
635 | // succeed but should fail) but only in extremely rare cases caused by willfully
636 | // bad choice of delimiter.
637 | if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r {
638 | return true
639 | }
640 | return false
641 | }
642 |
643 | // lexChar scans a character constant. The initial quote is already
644 | // scanned. Syntax checking is done by the parser.
645 | func lexChar(l *lexer) stateFn {
646 | Loop:
647 | for {
648 | switch l.next() {
649 | case '\\':
650 | if r := l.next(); r != eof && r != '\n' {
651 | break
652 | }
653 | fallthrough
654 | case eof, '\n':
655 | return l.errorf("unterminated character constant")
656 | case '\'':
657 | break Loop
658 | }
659 | }
660 | l.emit(itemCharConstant)
661 | return lexInsideAction
662 | }
663 |
664 | // lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
665 | // isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
666 | // and "089" - but when it's wrong the input is invalid and the parser (via
667 | // strconv) will notice.
668 | func lexNumber(l *lexer) stateFn {
669 | if !l.scanNumber() {
670 | return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
671 | }
672 |
673 | l.emit(itemNumber)
674 | return lexInsideAction
675 | }
676 |
677 | func (l *lexer) scanNumber() bool {
678 | // Optional leading sign.
679 | l.accept("+-")
680 | // Is it hex?
681 | digits := "0123456789"
682 | if l.accept("0") && l.accept("xX") {
683 | digits = "0123456789abcdefABCDEF"
684 | }
685 | l.acceptRun(digits)
686 | if l.accept(".") {
687 | l.acceptRun(digits)
688 | }
689 | if l.accept("eE") {
690 | l.accept("+-")
691 | l.acceptRun("0123456789")
692 | }
693 | //Is it imaginary?
694 | l.accept("i")
695 | //Next thing mustn't be alphanumeric.
696 | if isAlphaNumeric(l.peek()) {
697 | l.next()
698 | return false
699 | }
700 | return true
701 | }
702 |
703 | // lexQuote scans a quoted string.
704 | func lexQuote(l *lexer) stateFn {
705 | Loop:
706 | for {
707 | switch l.next() {
708 | case '\\':
709 | if r := l.next(); r != eof && r != '\n' {
710 | break
711 | }
712 | fallthrough
713 | case eof, '\n':
714 | return l.errorf("unterminated quoted string")
715 | case '"':
716 | break Loop
717 | }
718 | }
719 | l.emit(itemString)
720 | return lexInsideAction
721 | }
722 |
723 | // lexRawQuote scans a raw quoted string.
724 | func lexRawQuote(l *lexer) stateFn {
725 | Loop:
726 | for {
727 | switch l.next() {
728 | case eof:
729 | return l.errorf("unterminated raw quoted string")
730 | case '`':
731 | break Loop
732 | }
733 | }
734 | l.emit(itemRawString)
735 | return lexInsideAction
736 | }
737 |
738 | // isSpace reports whether r is a space character.
739 | func isSpace(r rune) bool {
740 | return r == ' ' || r == '\t' || r == '\r' || r == '\n'
741 | }
742 |
743 | // isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
744 | func isAlphaNumeric(r rune) bool {
745 | return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
746 | }
747 |
748 | // rightTrimLength returns the length of the spaces at the end of the string.
749 | func rightTrimLength(s string) Pos {
750 | return Pos(len(s) - len(strings.TrimRightFunc(s, isSpace)))
751 | }
752 |
753 | // leftTrimLength returns the length of the spaces at the beginning of the string.
754 | func leftTrimLength(s string) Pos {
755 | return Pos(len(s) - len(strings.TrimLeftFunc(s, isSpace)))
756 | }
757 |
758 | // atRightDelim reports whether the lexer is at a right delimiter, possibly preceded by a trim marker.
759 | func (l *lexer) atRightDelim() (delim, trimSpaces bool) {
760 | if strings.HasPrefix(l.input[l.pos:], l.trimRightDelim) { // With trim marker.
761 | return true, true
762 | }
763 | if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { // Without trim marker.
764 | return true, false
765 | }
766 | return false, false
767 | }
768 |
--------------------------------------------------------------------------------
/docs/syntax.md:
--------------------------------------------------------------------------------
1 | # Syntax Reference
2 |
3 | - [Delimiters](#delimiters)
4 | - [Whitespace Trimming](#whitespace-trimming)
5 | - [Comments](#comments)
6 | - [Variables](#variables)
7 | - [Initialization](#initialization)
8 | - [Assignment](#assignment)
9 | - [Expressions](#expressions)
10 | - [Identifiers](#identifiers)
11 | - [Indexing](#indexing)
12 | - [String](#string)
13 | - [Slice / Array](#slice--array)
14 | - [Map](#map)
15 | - [Struct](#struct)
16 | - [Field access](#field-access)
17 | - [Map](#map-1)
18 | - [Struct](#struct-1)
19 | - [Slicing](#slicing)
20 | - [Arithmetic](#arithmetic)
21 | - [String concatenation](#string-concatenation)
22 | - [Logical operators](#logical-operators)
23 | - [Ternary operator](#ternary-operator)
24 | - [Method calls](#method-calls)
25 | - [Function calls](#function-calls)
26 | - [Prefix syntax](#prefix-syntax)
27 | - [Pipelining](#pipelining)
28 | - [Piped argument slot](#piped-argument-slot)
29 | - [Control Structures](#control-structures)
30 | - [if](#if)
31 | - [if / else](#if--else)
32 | - [if / else if](#if--else-if)
33 | - [if / else if / else](#if--else-if--else)
34 | - [range](#range)
35 | - [Slices / Arrays](#slices--arrays)
36 | - [Maps](#maps)
37 | - [Channels](#channels)
38 | - [Custom](#custom-ranger)
39 | - [else](#else)
40 | - [try](#try)
41 | - [try / catch](#try--catch)
42 | - [Templates](#templates)
43 | - [include](#include)
44 | - [return](#return)
45 | - [Blocks](#blocks)
46 | - [block](#block)
47 | - [yield](#yield)
48 | - [content](#content)
49 | - [Recursion](#recursion)
50 | - [extends](#extends)
51 | - [import](#import)
52 |
53 | ## Delimiters
54 |
55 | Template delimiters are `{{` and `}}`. Delimiters can use `.` to output the execution context, and they can also contain many different literals, expressions, and declarations.
56 |
57 | hello {{ . }}
58 |
59 | Jet can also be configured to use alternative delimiters, such as `[[` and `]]`.
60 |
61 | hello [[ . ]]
62 |
63 | ### Whitespace Trimming
64 |
65 | By default, all text outside of template delimiters is copied verbatim when the template is parsed and executed, including whitespace.
66 |
67 | foo {{ "bar" }} baz
68 |
69 | To aid in formatting template source code, Jet can be instructed to trim whitepsace preceding and following delimiters using the `{{- ` and ` -}}` syntax. (This could be `[[- ` and ` -]]` with alternate delimiters, for example.) Note the space character adjacent to the dash which must be present.
70 |
71 | foo {{- "bar" -}} baz
72 |
73 | For this trimming, whitespace is defined as: spaces, horizontal tabs, carriage returns, and newlines.
74 |
75 | ### Comments
76 |
77 | Comments begin with `{*` and end with `*}` and will simply be dropped during template parsing.
78 |
79 | {* this is a comment *}
80 |
81 | Comments can span multiple lines:
82 |
83 | {*
84 | none of this will be executed:
85 | {{ asd }}
86 | {{ include "./foo.jet" }}
87 | *}
88 |
89 | ## Variables
90 |
91 | ### Initialization
92 |
93 | Variables have to be initialised before they can be used:
94 |
95 | {{ foo := "bar" }}
96 |
97 | ### Assignment
98 |
99 | Initialised variables can be assigned a new value:
100 |
101 | {{ foo = "asd" }}
102 |
103 | Variables initialised inside a template have no fixed type, so this is valid, too:
104 |
105 | {{ foo = 4711 }}
106 |
107 | Assigning anything to `_` tells Jet to evalute the right side, but skip the actual assignment to a new or existing identifier. This is useful to call a function but discard its return value:
108 |
109 | {{ _ := stillRuns() }}
110 | {{ _ = stillRuns() }}
111 |
112 | Since no actual assigning takes place, both of the above are equivalent: `stillRuns` is executed, but the return value will neither be stored in a variable, nor will it be rendered (unlike `{{ stillRuns() }}`, which would render the return value to the output).
113 |
114 | ## Expressions
115 |
116 | ### Identifiers
117 |
118 | Function and variable names are identifiers. Identifiers simply evaluate to the value stored for them in a variable scope, the globals, or the default variables. For example, the following are identifiers that resolve to built-in functions:
119 |
120 | - `len`
121 | - `isset`
122 | - `split`
123 |
124 | After `{{ foo := "foo" }}`, the `foo` in `{{ len(foo) }}` is an identifier expression and resolved to the string "foo".
125 |
126 | ### Indexing
127 |
128 | Indexing expressions use `[]` syntax and evaluate to a byte in a string, an element in a slice or array, a value in a map, or a field of a struct.
129 |
130 | #### String
131 |
132 | {{ s := "helloworld" }}
133 | {{ s[1] }}
134 |
135 | #### Slice / Array
136 |
137 | {{ s := slice("foo", "bar", "asd") }}
138 | {{ s[0] }}
139 | {{ i := 2 }}
140 | {{ s[i] }}
141 |
142 | #### Map
143 |
144 | {{ m := map("foo", 123, "bar", 456) }}
145 | {{ m["foo"] }}
146 | {{ bar := "bar" }}
147 | {{ m[bar] }}
148 |
149 | #### Struct
150 |
151 | Assuming `user` is a Go struct value with a string field "Name":
152 |
153 | {{ user["Name"] }}
154 |
155 | ### Field access
156 |
157 | Field access expressions use dot notation (`foo.bar`) and can be used with maps or structs. When the identifier in front of the `.` is omitted, the field is looked up in the current context (which will fail if the context is not a map or struct).
158 |
159 | #### Map
160 |
161 | {{ m := map("foo", 123, "bar", 456) }}
162 | {{ m.foo }}
163 | {{ s := slice(m, map("foo", 4711)) }}
164 | {{ range s }}
165 | {{ .foo }}
166 | {{ end }}
167 |
168 | #### Struct
169 |
170 | Assuming `user` is a Go struct value with a string field "Name":
171 |
172 | {{ user.Name }}
173 |
174 | Assuming `users` is a slice of Go structs, o:
175 |
176 | {{ range users }}
177 | {{ .Name }}
178 | {{ end }}
179 |
180 | ### Slicing
181 |
182 | You may re-slice a slice or array using the Go-like [start:end] syntax. The element at the `start` index will be included, the one at the `end` index will be excluded.
183 |
184 | {{ s := slice(6, 7, 8, 9, 10, 11) }}
185 | {{ sevenEightNine := s[1:4] }}
186 |
187 | ### Arithmetic
188 |
189 | Basic arithmetic operators are supported: `+`, `-`, `*`, `/`, `%`
190 |
191 | {{ 1 + 2 * 3 - 4 }}
192 | {{ (1 + 2) * 3 - 4.1 }}
193 |
194 | ### String concatenation
195 |
196 | {{ "HELLO" + " " + "WORLD!" }}
197 |
198 | #### Logical operators
199 |
200 | The following operators are supported:
201 |
202 | - `&&`: and
203 | - `||`: or
204 | - `!`: not
205 | - `==`: equal
206 | - `!=`: not equal
207 | - `>`: greater than
208 | - `>=`: greater than or equal (= not less than)
209 | - `<`: less than
210 | - `<=`: less than or equal (= not greater than)
211 |
212 | Examples:
213 |
214 | {{ item == true || !item2 && item3 != "test" }}
215 |
216 | {{ item >= 12.5 || item < 6 }}
217 |
218 | Logical expressions always evaluate to either `true` or `false`.
219 |
220 | ### Ternary operator
221 |
222 | ` x ? y : z` evaluates to `y` if `x` is truthy or `z` otherwise.
223 |
224 | {{ .HasTitle ? .Title : "Title not set" }}
225 |
226 | ### Method calls
227 |
228 | You can call exported methods of Go types:
229 |
230 | {{ user.Rename("Peter") }}
231 | {{ range users }}
232 | {{ .FullName() }}
233 | {{ end }}
234 |
235 | ### Function calls
236 |
237 | Function calls can be written using familiar C-like syntax:
238 |
239 | {{ len(s) }}
240 | {{ isset(foo, bar) }}
241 |
242 | #### Prefix syntax
243 |
244 | Function calls can also be written using a colon instead of parentheses:
245 |
246 | {{ len: s }}
247 | {{ isset: foo, bar }}
248 |
249 | Note that function calls using this syntax can't be nested! This is valid: `{{ len: slice("asd", "foo") }}`, but this isn't: `{{ len: slice: "asd", "foo" }}`
250 |
251 | #### Pipelining
252 |
253 | Pipelining works by "piping" a value into a function as its first argument:
254 |
255 | {{ "123" | len}}
256 | {{ "FOO" | lower | len }}
257 |
258 | Pipelines are evaluated left-to-right. This chaining syntax may be easier to read than deeply nested calls:
259 |
260 | {{ "123" | lower | upper | len }}
261 |
262 | is equivalent to
263 |
264 | {{ len(upper(lower("123"))) }}
265 |
266 | Inside a pipeline, functions can be enriched with additional parameters:
267 |
268 | {{ "hello" | repeat: 2 | len }}
269 | {{ "hello" | repeat(2) | len }}
270 |
271 | Both of the above are equivalent to this:
272 |
273 | {{ len(repeat("hello", 2)) }}
274 |
275 | Please note that the raw, unsafe, safeHtml or safeJs built-in escapers (or custom safe writers) need to be the last command evaluated in an action node. This means they have to come last when used in a pipeline.
276 |
277 | {{ "hello" | upper | raw }}
278 | {{ raw: "hello" }}
279 | {{ raw: "hello" | upper }}
280 |
281 | #### Piped argument slot
282 |
283 | When pipelining, it can be desirable to use the piped value in a different slot in the function call than the first. To tell Jet where to inject the piped value, use `_`:
284 |
285 | {{ 2 | repeat("foo", _) }}
286 | {{ 2 | repeat("foo", _) | repeat(_, 3) }}
287 |
288 | All of the following produce the same output as the second line in the example above:
289 |
290 | {{ 2 | repeat("foo", _) | repeat(3) }}
291 | {{ 2 | repeat: "foo", _ | repeat: 3 }}
292 | {{ 2 | repeat: "foo", _ | repeat: _, 3 }}
293 |
294 | This feature is inspired by [function capturing](https://gleam.run/tour/functions.html#function-capturing) in Gleam.
295 |
296 | ## Control Structures
297 |
298 | ### if
299 |
300 | You can branch inside templates depending on a condition using `if`:
301 |
302 | {{ if foo == "asd" }}
303 | foo is 'asd'!
304 | {{ end }}
305 |
306 | #### if / else
307 |
308 | You may provide an `else` block when using `if`:
309 |
310 | {{ if foo == "asd" }}
311 | foo is 'asd'!
312 | {{ else }}
313 | foo is something else!
314 | {{ end }}
315 |
316 | #### if / else if
317 |
318 | You can test for another condition using `else if`:
319 |
320 | {{ if foo == "asd" }}
321 | foo is 'asd'!
322 | {{ else if foo == 4711 }}
323 | foo is 4711!
324 | {{ end }}
325 |
326 | This is exactly the same as this code:
327 |
328 | {{ if foo == "asd" }}
329 | foo is 'asd'!
330 | {{ else }}
331 | {{ if foo == 4711 }}
332 | foo is 4711!
333 | {{ end }}
334 | {{ end }}
335 |
336 |
337 | #### if / else if / else
338 |
339 | `if / else if / else` works, too, of course:
340 |
341 | {{ if foo == "asd" }}
342 | foo is 'asd'!
343 | {{ else if foo == 4711 }}
344 | foo is 4711!
345 | {{ else }}
346 | foo is something else!
347 | {{ end }}
348 |
349 | and will do exactly the same as this:
350 |
351 | {{ if foo == "asd" }}
352 | foo is 'asd'!
353 | {{ else }}
354 | {{ if foo == 4711 }}
355 | foo is 4711!
356 | {{ else }}
357 | foo is something else!
358 | {{ end }}
359 | {{ end }}
360 |
361 | ### range
362 |
363 | Use `range` to iterate over data, just like you would in Go, or how you would use a `foreach` loop in other programming languages. Inside the `range`, the context (`.`) is set to the current iteration's value:
364 |
365 | {{ s := slice("foo", "bar", "asd") }}
366 | {{ range s }}
367 | {{.}}
368 | {{ end }}
369 |
370 | Jet provides built-in rangers for Go slices, arrays, maps, and channels. You can add your own by implementing the Ranger interface.
371 |
372 | #### Slices / Arrays
373 |
374 | When iterating over a slice or array, Jet can give you the current iteration index:
375 |
376 | {{ range i := s }}
377 | {{i}}: {{.}}
378 | {{ end }}
379 |
380 | If you want, you can have Jet assign the iteration value to another value:
381 |
382 | {{ range i, v := s }}
383 | {{i}}: {{v}}
384 | {{ end }}
385 |
386 | The iteration value will then not be used as context (`.`); instead, the parent context remains available.
387 |
388 | #### Maps
389 |
390 | When iterating over a map, Jet can give you the key current iteration index:
391 |
392 | {{ m := map("foo", "bar", "asd", 123)}}
393 | {{ range k := m }}
394 | {{k}}: {{.}}
395 | {{ end }}
396 |
397 | Just like with slices, you can have Jet assign the iteration value to another value:
398 |
399 | {{ range k, v := m }}
400 | {{k}}: {{v}}
401 | {{ end }}
402 |
403 | The iteration value will then not be used as context (`.`); instead, the parent context remains available.
404 |
405 | #### Channels
406 |
407 | When iterating over a channel, you can have Jet assign the iteration value to another value in order to keep the parent context, similar to the two-variables syntax for slices and maps:
408 |
409 | {{ range v := c }}
410 | {{v}}
411 | {{ end }}
412 |
413 | It's an error to use channels together with the two-variable syntax.
414 |
415 | #### Custom Ranger
416 |
417 | Any value that implements the
418 | [Ranger](https://pkg.go.dev/github.com/CloudyKit/jet/v6#Ranger) interface can be
419 | used for ranging over values. Look in the package docs for an example.
420 |
421 | #### else
422 |
423 | `range` statements can have an `else` block which is executed if there are non values to range over (as signalled by the Ranger). For example, it will run when iterating an empty slice, array or map or a closed channel:
424 |
425 | {{ range searchResults }}
426 | {{.}}
427 | {{ else }}
428 | No results found :(
429 | {{ end }}
430 |
431 | ### try
432 |
433 | If you want to attempt rendering something, but don't want Jet to crash when something goes wrong, you can use `try`:
434 |
435 | {{ try }}
436 | we're not sure if we already initialised foo,
437 | so the next line might fail...
438 | {{ foo }}
439 | {{ end }}
440 |
441 | You can do anything you want inside a `try` block, even yield blocks or include other templates.
442 |
443 | All render output generated inside the `try` block is buffered and only included in the surrounding output after execution of the entire block completed successfully. Any runtime error means no content from inside `try` is kept.
444 |
445 | ### try / catch
446 |
447 | In case of an error inside the `try` block, you can have Jet evaluate a `catch` block:
448 |
449 | {{ try }}
450 | we're not sure if we already initialised foo,
451 | so the next line might fail...
452 | {{ foo }}
453 | {{ catch }}
454 | foo was not initialised, this is fallback content
455 | {{ end }}
456 |
457 | Errors occuring inside the `catch` block are not caught and will cause execution to abort.
458 |
459 | You can also have the error that occured assigned to a variable inside the `catch` block to log it or otherwise handle it. Since it's a Go error value, you have to call `.Error()` on it to get the error message as a string.
460 |
461 | {{ try }}
462 | we're not sure if we already initialised foo,
463 | so the next line might fail...
464 | {{ foo }}
465 | {{ catch err }}
466 | {{ log(err.Error()) }}
467 | uh oh, something went wrong: {{ err.Error() }}
468 | {{ end }}
469 |
470 | `err` will not be available outside the `catch` block.
471 |
472 | ## Templates
473 |
474 | ### include
475 |
476 | Including a template is similar to using partials in other template languages. All local and global variables are available to you in the included template. You can pass a context by specifying it as the last argument in the `include` statement. If you don't pass a context, the current context will be kept.
477 |
478 |
479 |
503 |
504 | ### return
505 |
506 | Templates can set a value as their return value using `return`. This is only useful when the template was executed using the `exec()` built-in function, which will make the return value of a template available in another template.
507 |
508 | `return` will **not** stop execution of the current block or template!
509 |
510 |
511 | {{ f := "f" }}
512 | {{ o := "o" }}
513 | {{ return f+o+o }}
514 |
515 |
516 | {{ foo := exec("./foo.jet") }}
517 | Hello, {{ foo }}!
518 |
519 | The output will simply be:
520 |
521 | Hello, foo!
522 |
523 | ## Blocks
524 |
525 | You can think of blocks as partials or pieces of a template that you can invoke by name.
526 |
527 | ### block
528 |
529 | To define a block, use `block`:
530 |
531 | {{block copyright()}}
532 |
533 | {{end}}
534 |
535 | Defining a block in a template that's being executed will also invoke it immediately. To avoid this, use `import` or `extends`. Blocks can't be named "content", "yield", or other Jet keywords.
536 |
537 | A block definition accepts a comma-separated list of argument names, with optional defaults:
538 |
539 | {{ block inputField(type="text", label, id, value="", required=false) }}
540 |
541 |
542 |
543 |
544 | {{ end }}
545 |
546 | ### yield
547 |
548 | To invoke a previously defined block, use `yield`:
549 |
550 |
553 |
554 | {{yield inputField(id="firstname", label="First name", required=true)}}
555 |
556 | The sequence of parameters is irrelevant, and parameters without a default value must be passed when yielding a block.
557 |
558 | You can pass something to be used as context, or the current context will be passed. Given
559 |
560 | {{block buff()}}
561 | {{.}}
562 | {{end}}
563 |
564 | the following invocation
565 |
566 | {{yield buff() "Batman"}}
567 |
568 | will produce
569 |
570 | Batman
571 |
572 | ### content
573 |
574 | When defining a block, use the special `{{ yield content }}` statement to designate where any inner content should be rendered. Then, when you invoke the block with yield, use the keyword content at the end of the `yield`. For example:
575 |
576 | {{ block link(target) }}
577 | {{ yield content }}
578 | {{ end }}
579 |
580 | [...]
581 |
582 | {{ yield link(target="https://www.example.com") content }}
583 | Example Inc.
584 | {{ end }}
585 |
586 | The output will be
587 |
588 | Example Inc.
589 |
590 | The invocating `yield` (`{{ yield link(target="https://www.example.com") content }}`) will store the content (together with the current variable scope) and the `{{ yield content }}` part will restore the variable scope and execute the content. When you pass a context during the `yield` block invocation, it will be used when executing the content as well.
591 |
592 | Since defining a block will also invoke it, you can also define some content immediately as part of the `block` definition:
593 |
594 | {{ name := "Sarah" }}
595 | {{ block header() }}
596 |
597 | {{ yield content }}
598 |
599 | {{ content }}
600 |
Hey {{ name }}!
601 | {{ end }}
602 |
603 | This will render something like the following at the position where the block is defined:
604 |
605 |
606 |
Hey Sarah!
607 |
608 |
609 | ### Recursion
610 |
611 | You can yield a block inside its own definition:
612 |
613 | {{ block menu() }}
614 |
615 | {{ range . }}
616 |
{{ .Text }}{{ if len(.Children) }}{{ yield menu() .Children }}{{ end }}
617 | {{ end }}
618 |
619 | {{ end }}
620 |
621 | ### extends
622 |
623 | A template can extend another template using an `extends` statement followed by a template path at the very top:
624 |
625 |
626 | {{extends "./layout.jet"}}
627 |
628 | In an extending template, content outside of a block definition will be discarded:
629 |
630 |
631 | {{extends "./layout.jet"}}
632 | {{block body()}}
633 |
634 | This content can be yielded anywhere.
635 |
636 | {{end}}
637 | This content will never be rendered.
638 |
639 | The extended template then has to have yield slots to render your blocks into:
640 |
641 |
642 |
643 |
644 |
645 | {{yield body()}}
646 |
647 |
648 |
649 | The final result will be:
650 |
651 |
652 |
653 |
654 |
655 | This content can be yielded anywhere.
656 |
657 |
658 |
659 |
660 | Every template can only extend one other template, and the `extends` statement has to be at the very top of the file (even above `import` statements).
661 |
662 | Since the extending template isn't actually executed (the extended template is), the blocks defined in it don't run until you `yield` them explicitely.
663 |
664 | ### import
665 |
666 | A template's defined blocks can be imported into another template using the `import` statement:
667 |
668 |
669 | {{ block body() }}
670 |
671 | This content can be yielded anywhere.
672 |
673 | {{ end }}
674 |
675 |
676 | {{ import "./my_blocks.jet" }}
677 |
678 |
679 |
680 | {{ yield body() }}
681 |
682 |
683 |
684 | Executing `index.jet` will produce:
685 |
686 |
687 |
688 |
689 |
690 | This content can be yielded anywhere.
691 |
692 |
693 |
694 |
695 | `import` makes all the blocks from the imported template available in the importing template. There is no way to only import (a) specific block(s).
696 |
697 | Since the imported template isn't actually executed, the blocks defined in it don't run until you `yield` them explicitely.
698 |
--------------------------------------------------------------------------------