1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-07 12:07:28 +00:00

Implement a new View Engine for the Jet template parser as requested at: https://github.com/kataras/iris/issues/1281

Former-commit-id: 3e00bdfbf1f3998a1744c390c12fd70430ac0320
This commit is contained in:
Gerasimos (Makis) Maropoulos
2019-06-22 21:34:19 +03:00
parent b71d4032e6
commit 076d9121f1
22 changed files with 1124 additions and 40 deletions

View File

@@ -339,6 +339,7 @@ Follow the examples below,
| handlebars | `iris.Handlebars(...)` |
| amber | `iris.Amber(...)` |
| pug(jade) | `iris.Pug(...)` |
| jet | `iris.Jet(...)` |
- [Overview](view/overview/main.go)
- [Hi](view/template_html_0/main.go)
@@ -353,6 +354,8 @@ Follow the examples below,
- [Pug (Jade) Actions`](view/template_pug_1)
- [Pug (Jade) Includes`](view/template_pug_2)
- [Pug (Jade) Extends`](view/template_pug_3)
- [Jet](/view/template_jet_0) **NEW**
- [Jet Embedded](view/template_jet_1_embedded) **NEW**
You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero templates](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/herotemplate](http_responsewriter/herotemplate) examples.

View File

@@ -32,7 +32,7 @@ func main() {
sess := sessions.New(sessions.Config{
Cookie: "sessionscookieid",
Expires: 0, // defauls to 0: unlimited life. Another good value is: 45 * time.Minute,
Expires: 0, // defaults to 0: unlimited life. Another good value is: 45 * time.Minute,
AllowReclaim: true,
})

View File

@@ -5,11 +5,6 @@ import "github.com/kataras/iris"
func main() {
app := iris.New()
// - standard html | iris.HTML(...)
// - django | iris.Django(...)
// - pug(jade) | iris.Pug(...)
// - handlebars | iris.Handlebars(...)
// - amber | iris.Amber(...)
// with default template funcs:
//
// - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }}

View File

@@ -7,12 +7,6 @@ import (
func main() {
app := iris.New() // defaults to these
// - standard html | iris.HTML(...)
// - django | iris.Django(...)
// - pug(jade) | iris.Pug(...)
// - handlebars | iris.Handlebars(...)
// - amber | iris.Amber(...)
tmpl := iris.HTML("./templates", ".html")
tmpl.Reload(true) // reload templates on each request (development mode)
// default template funcs are:

View File

@@ -0,0 +1,11 @@
# Jet Engine Example
This example is a fork of <https://github.com/CloudyKit/jet/tree/master/examples/todos> to work with Iris, so you can
notice the differences side by side.
Read more at: https://github.com/kataras/iris/issues/1281
> The Iris Jet View Engine fixes some bugs that the underline [jet template parser](https://github.com/CloudyKit/jet) currently has.
Continue by learning how you can [serve embedded templates](../template_jet_1_embedded).

View File

@@ -0,0 +1,131 @@
// Package main shows how to use jet template parser with ease using the Iris built-in Jet view engine.
// This example is customized fork of https://github.com/CloudyKit/jet/tree/master/examples/todos, so you can
// notice the differences side by side.
package main
import (
"bytes"
"encoding/base64"
"fmt"
"os"
"reflect"
"strings"
"github.com/kataras/iris"
"github.com/kataras/iris/view"
)
type tTODO struct {
Text string
Done bool
}
type doneTODOs struct {
list map[string]*tTODO
keys []string
len int
i int
}
func (dt *doneTODOs) New(todos map[string]*tTODO) *doneTODOs {
dt.len = len(todos)
for k := range todos {
dt.keys = append(dt.keys, k)
}
dt.list = todos
return dt
}
// Range satisfies the jet.Ranger interface and only returns TODOs that are done,
// even when the list contains TODOs that are not done.
func (dt *doneTODOs) Range() (reflect.Value, reflect.Value, bool) {
for dt.i < dt.len {
key := dt.keys[dt.i]
dt.i++
if dt.list[key].Done {
return reflect.ValueOf(key), reflect.ValueOf(dt.list[key]), false
}
}
return reflect.Value{}, reflect.Value{}, true
}
func (dt *doneTODOs) Render(r *view.JetRuntime) {
r.Write([]byte(fmt.Sprintf("custom renderer")))
}
// Render implements jet.Renderer interface
func (t *tTODO) Render(r *view.JetRuntime) {
done := "yes"
if !t.Done {
done = "no"
}
r.Write([]byte(fmt.Sprintf("TODO: %s (done: %s)", t.Text, done)))
}
func main() {
//
// Type aliases:
// view.JetRuntimeVars = jet.VarMap
// view.JetRuntime = jet.Runtime
// view.JetArguments = jet.Arguments
//
// Iris also gives you the ability to put runtime variables
// from middlewares as well, by:
// view.AddJetRuntimeVars(ctx, vars)
// or tmpl.AddRuntimeVars(ctx, vars)
//
// The Iris Jet fixes the issue when custom jet.Ranger and custom jet.Renderer are not actually work at all,
// hope that this is a temp issue on the jet parser itself and authors will fix it soon
// so we can remove the "hacks" we've putted for those to work as expected.
app := iris.New()
tmpl := iris.Jet("./views", ".jet") // <--
tmpl.Reload(true) // remove in production.
tmpl.AddFunc("base64", func(a view.JetArguments) reflect.Value {
a.RequireNumOfArguments("base64", 1, 1)
buffer := bytes.NewBuffer(nil)
fmt.Fprint(buffer, a.Get(0))
return reflect.ValueOf(base64.URLEncoding.EncodeToString(buffer.Bytes()))
})
app.RegisterView(tmpl) // <--
var todos = map[string]*tTODO{
"example-todo-1": {Text: "Add an show todo page to the example project", Done: true},
"example-todo-2": {Text: "Add an add todo page to the example project"},
"example-todo-3": {Text: "Add an update todo page to the example project"},
"example-todo-4": {Text: "Add an delete todo page to the example project", Done: true},
}
app.Get("/", func(ctx iris.Context) {
ctx.View("todos/index.jet", todos) // <--
// Note that the `ctx.View` already logs the error if logger level is allowing it and returns the error.
})
app.Get("/todo", func(ctx iris.Context) {
id := ctx.URLParam("id")
todo, ok := todos[id]
if !ok {
ctx.Redirect("/")
return
}
ctx.View("todos/show.jet", todo)
})
app.Get("/all-done", func(ctx iris.Context) {
vars := make(view.JetRuntimeVars) // <-- or keep use the jet.VarMap, decision up to you, it refers to the same type.
vars.Set("showingAllDone", true)
view.AddJetRuntimeVars(ctx, vars) // <--
ctx.View("todos/index.jet", (&doneTODOs{}).New(todos))
})
port := os.Getenv("PORT")
if len(port) == 0 {
port = ":8080"
} else if !strings.HasPrefix(":", port) {
port = ":" + port
}
app.Run(iris.Addr(port))
}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ isset(title) ? title : "" }}</title>
</head>
<body>
{{block documentBody()}}{{end}}
</body>
</html>

View File

@@ -0,0 +1,30 @@
{{extends "layouts/application.jet"}}
{{block button(label, href="javascript:void(0)")}}
<a href="{{ href }}">{{ label }}</a>
{{end}}
{{block ul()}}
<ul>
{{yield content}}
</ul>
{{end}}
{{block documentBody()}}
<h1>List of TODOs</h1>
{{if isset(showingAllDone) && showingAllDone}}
<p>Showing only TODOs that are done</p>
{{else}}
<p><a href="/all-done">Show only TODOs that are done</a></p>
{{end}}
{{yield ul() content}}
{{range id, value := .}}
<li {{if value.Done}}style="color:red;text-decoration: line-through;"{{end}}>
<a href="/todo?id={{ id }}">{{ value.Text }}</a>
{{yield button(label="UP", href="/update/?id="+base64(id))}} - {{yield button(href="/delete/?id="+id, label="DL")}}
</li>
{{end}}
{{end}}
{{end}}

View File

@@ -0,0 +1,9 @@
{{extends "layouts/application.jet"}}
{{block documentBody()}}
<h1>Show TODO</h1>
<p>This uses a custom renderer by implementing the jet.Renderer interface.
<p>
{{.}}
</p>
{{end}}

View File

@@ -0,0 +1,25 @@
# Jet Engine Example (Embedded)
Take a look at the [../template_jet_0](../template_jet_0)'s README first.
This example teaches you how to use jet templates embedded in your applications with ease using the Iris built-in Jet view engine.
This example is a customized fork of https://github.com/CloudyKit/jet/tree/master/examples/asset_packaging, so you can
notice the differences side by side. For example, you don't have to use any external package inside your application,
Iris manually builds the template loader for binary data when Asset and AssetNames are available through tools like the [go-bindata](github.com/shuLhan/go-bindata).
Note that you can still use any custom loaders through the `JetEngine.SetLoader`
which overrides any previous loaders like `JetEngine.Binary` we use on this example.
## How to run
```sh
$ go get -u github.com/shuLhan/go-bindata/... # or any active alternative
$ go-bindata ./views/...
$ go build
$ ./template_jet_0_embedded
```
Repeat the above steps on any `./views` changes.
> html files are not used, only binary data. You can move or delete the `./views` folder.

View File

@@ -0,0 +1,387 @@
// Code generated by go-bindata. DO NOT EDIT.
// sources:
// views\includes\_partial.jet
// views\includes\blocks.jet
// views\index.jet
// views\layouts\application.jet
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info fileInfoEx
}
type fileInfoEx interface {
os.FileInfo
MD5Checksum() string
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
md5checksum string
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) MD5Checksum() string {
return fi.md5checksum
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _bindataViewsincludespartialjet = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\x29\xb0\xf3\xcc\x4b\xce\x29\x4d\x49\x4d\x51\x28\x48\x2c\x2a\xc9\x4c" +
"\xcc\xb1\xd1\x2f\xb0\xe3\x02\x04\x00\x00\xff\xff\xd0\x10\x20\x0a\x18\x00\x00\x00")
func bindataViewsincludespartialjetBytes() ([]byte, error) {
return bindataRead(
_bindataViewsincludespartialjet,
"views/includes/_partial.jet",
)
}
func bindataViewsincludespartialjet() (*asset, error) {
bytes, err := bindataViewsincludespartialjetBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{
name: "views/includes/_partial.jet",
size: 24,
md5checksum: "",
mode: os.FileMode(438),
modTime: time.Unix(1533831061, 0),
}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _bindataViewsincludesblocksjet = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\x4e\xca\xc9\x4f\xce\x56\xc8\x4d\xcd\x2b\xd5\xd0\xac\xad\xe5\x52" +
"\x50\xb0\x29\xb0\x0b\xc9\x48\x05\x8b\x28\x40\x24\xcb\x13\x8b\x15\x32\xf3\xca\xf2\xb3\x53\x53\xf4\x6c\xf4\x0b\xec" +
"\xb8\xaa\xab\x53\xf3\x52\x6a\x6b\xb9\x00\x01\x00\x00\xff\xff\x3f\xde\x27\x27\x3e\x00\x00\x00")
func bindataViewsincludesblocksjetBytes() ([]byte, error) {
return bindataRead(
_bindataViewsincludesblocksjet,
"views/includes/blocks.jet",
)
}
func bindataViewsincludesblocksjet() (*asset, error) {
bytes, err := bindataViewsincludesblocksjetBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{
name: "views/includes/blocks.jet",
size: 62,
md5checksum: "",
mode: os.FileMode(438),
modTime: time.Unix(1533831061, 0),
}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _bindataViewsindexjet = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\x90\xb1\x6e\xf3\x30\x0c\x84\x77\x3d\xc5\xfd\x5e\xfe\x74\xa9\x91\xb5" +
"\x30\x3c\x14\xc8\xd0\xa5\x4b\x1f\xa0\x50\x2c\x06\x56\x23\x8b\x42\x48\xd7\x36\x04\xbf\x7b\x11\xc5\x29\x8a\x76\x22" +
"\xc8\xfb\x8e\x3c\x30\x67\x9a\x95\xa2\x13\x54\xc1\x2e\x3c\xaa\xd4\x36\xa5\xe0\x3b\xab\x9e\xe3\xe3\x07\x69\xb5\xae" +
"\x26\x67\x3f\x24\xbe\x28\x2a\x1f\xbb\x30\x3a\x92\xfa\x18\xb8\x3b\xcb\x1d\x30\x39\x97\x01\x1c\x77\xe3\x40\x51\x9f" +
"\xd9\x2d\xbb\x87\x75\x35\x40\xd3\xef\xdb\xc3\x70\x24\xe7\xc8\x81\x66\x3b\xa4\x40\x4d\xdd\xef\x5b\x73\x15\xa3\xfd" +
"\x6c\x0d\x00\xe4\xbc\x78\x0a\x0e\x03\xc5\xf1\xee\xac\x8b\x6a\xae\xe2\x76\xf8\x47\x82\xf7\x64\x2f\xea\x6d\xf8\xce" +
"\x50\xb0\x13\xfe\x6d\xc4\xcb\xe9\x30\x7b\x51\xd9\x55\x8e\x49\x5e\x59\x4b\x5b\xe8\xdb\x7a\xa0\x49\xed\x5b\xcf\x93" +
"\xa0\xe7\xe9\x8f\x0f\x13\x5f\xce\xf2\x84\xdf\x6e\x4c\x56\x10\x59\xb1\xf1\x0e\x47\xea\xec\x28\x04\xaf\x05\x8e\xff" +
"\x15\x54\xe8\xa6\x4e\x6d\x89\x45\xd1\x95\x37\xde\xea\x57\x00\x00\x00\xff\xff\xff\xe0\x1f\xae\x75\x01\x00\x00")
func bindataViewsindexjetBytes() ([]byte, error) {
return bindataRead(
_bindataViewsindexjet,
"views/index.jet",
)
}
func bindataViewsindexjet() (*asset, error) {
bytes, err := bindataViewsindexjetBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{
name: "views/index.jet",
size: 373,
md5checksum: "",
mode: os.FileMode(438),
modTime: time.Unix(1561227802, 0),
}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _bindataViewslayoutsapplicationjet = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x34\x8e\xbd\xae\xc2\x30\x0c\x85\xf7\x3e\x85\x6f\xa6\xcb\x80\xba\x32\xb8" +
"\x1d\x80\xb2\xc2\x50\x06\xc6\x34\xb6\x08\x22\x3f\x08\x8c\x44\x15\xe5\xdd\x51\x08\x4c\xb6\xec\xf3\x7d\x3a\xf8\xb7" +
"\xdd\x6f\xc6\xd3\x61\x00\x2b\xde\xf5\x0d\x96\x01\x4e\x87\x73\xa7\x38\xa8\xbe\x01\x40\xcb\x9a\xca\x02\x80\x9e\x45" +
"\x83\xb1\xfa\xfe\x60\xe9\xd4\x71\xdc\x2d\x57\xea\xfb\x92\x8b\x38\xee\x07\x3f\x31\x11\x13\xf0\x4b\xfb\x9b\x63\x6c" +
"\xeb\xbd\x78\xda\x9f\x08\xa7\x48\x73\xc5\x52\x9a\x5c\x34\x57\xa0\x68\x9e\x9e\x83\xac\x23\xcd\xff\x8b\x9c\x53\xe2" +
"\x40\x39\x7f\xb0\x9a\xc6\xb6\x36\x7c\x07\x00\x00\xff\xff\x76\x86\x91\x20\xb2\x00\x00\x00")
func bindataViewslayoutsapplicationjetBytes() ([]byte, error) {
return bindataRead(
_bindataViewslayoutsapplicationjet,
"views/layouts/application.jet",
)
}
func bindataViewslayoutsapplicationjet() (*asset, error) {
bytes, err := bindataViewslayoutsapplicationjetBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{
name: "views/layouts/application.jet",
size: 178,
md5checksum: "",
mode: os.FileMode(438),
modTime: time.Unix(1561227776, 0),
}
a := &asset{bytes: bytes, info: info}
return a, nil
}
//
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
//
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
//
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
// nolint: deadcode
//
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
//
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or could not be loaded.
//
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
//
// AssetNames returns the names of the assets.
// nolint: deadcode
//
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
//
// _bindata is a table, holding each asset generator, mapped to its name.
//
var _bindata = map[string]func() (*asset, error){
"views/includes/_partial.jet": bindataViewsincludespartialjet,
"views/includes/blocks.jet": bindataViewsincludesblocksjet,
"views/index.jet": bindataViewsindexjet,
"views/layouts/application.jet": bindataViewslayoutsapplicationjet,
}
//
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
//
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
}
}
if node.Func != nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{Func: nil, Children: map[string]*bintree{
"views": {Func: nil, Children: map[string]*bintree{
"includes": {Func: nil, Children: map[string]*bintree{
"_partial.jet": {Func: bindataViewsincludespartialjet, Children: map[string]*bintree{}},
"blocks.jet": {Func: bindataViewsincludesblocksjet, Children: map[string]*bintree{}},
}},
"index.jet": {Func: bindataViewsindexjet, Children: map[string]*bintree{}},
"layouts": {Func: nil, Children: map[string]*bintree{
"application.jet": {Func: bindataViewslayoutsapplicationjet, Children: map[string]*bintree{}},
}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

View File

@@ -0,0 +1,31 @@
// Package main shows how to use jet templates embedded in your application with ease using the Iris built-in Jet view engine.
// This example is a customized fork of https://github.com/CloudyKit/jet/tree/master/examples/asset_packaging, so you can
// notice the differences side by side. For example, you don't have to use any external package inside your application,
// Iris manually builds the template loader for binary data when Asset and AssetNames are available via tools like the go-bindata.
package main
import (
"os"
"strings"
"github.com/kataras/iris"
)
func main() {
app := iris.New()
tmpl := iris.Jet("./views", ".jet").Binary(Asset, AssetNames)
app.RegisterView(tmpl)
app.Get("/", func(ctx iris.Context) {
ctx.View("index.jet")
})
port := os.Getenv("PORT")
if len(port) == 0 {
port = ":8080"
} else if !strings.HasPrefix(":", port) {
port = ":" + port
}
app.Run(iris.Addr(port))
}

View File

@@ -0,0 +1 @@
<p>Included partial</p>

View File

@@ -0,0 +1,3 @@
{{block menu()}}
<p>The menu block was invoked.</p>
{{end}}

View File

@@ -0,0 +1,16 @@
{{extends "layouts/application.jet"}}
{{import "includes/blocks.jet"}}
{{block documentBody()}}
<h1>Embedded example</h1>
<nav>
{{yield menu()}}
</nav>
{{include "includes/_partial.jet"}}
{{if !includeIfExists("doesNotExist.jet")}}
<p>Shows how !includeIfExists works: doesNotExist.jet was not included because it doesn't exist.</p>
{{end}}
{{end}}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Embedded example</title>
</head>
<body>
{{block documentBody()}}{{end}}
</body>
</html>