diff --git a/README.md b/README.md
index b69be54..d0291ee 100644
--- a/README.md
+++ b/README.md
@@ -1,101 +1,47 @@
-# Jet Template Engine for GO [![Build Status](https://travis-ci.org/CloudyKit/jet.svg?branch=master)](https://travis-ci.org/CloudyKit/jet)
+# Jet Template Engine for Go [![Build Status](https://travis-ci.org/CloudyKit/jet.svg?branch=master)](https://travis-ci.org/CloudyKit/jet)
[![Join the chat at https://gitter.im/CloudyKit/jet](https://badges.gitter.im/CloudyKit/jet.svg)](https://gitter.im/CloudyKit/jet?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-Jet is a template engine developed to be easy to use, powerful, dynamic,secure and very fast.
+Jet is a template engine developed to be easy to use, powerful, dynamic, yet secure and very fast.
-* Support template inheritance extends,imports and includes statements.
-* Descriptive error messages with file name and line number.
-* Auto-escape.
-* Simple C like Expression.
-* Very fast execution, jet can execute templates faster then some pre-compiled template engines
-* Very light in terms of allocations and memory foot print.
-* Simple and familiar syntax.
-* Ease to use.
+* supports template inheritance with `extends`, `import` and `include` statements
+* descriptive error messages with filename and line number
+* auto-escape
+* simple C-like expressions
+* very fast execution – Jet can execute templates faster than some pre-compiled template engines
+* very light in terms of allocations and memory footprint
+* simple and familiar syntax
+* easy to use
-[Documentation Wiki](https://github.com/CloudyKit/jet/wiki)
+You can find the documentation in the [wiki](https://github.com/CloudyKit/jet/wiki).
-#### Intellij Plugin
+#### Upgrade to v2
-If you use intellij there is a plugin available in https://github.com/jhsx/GoJetPlugin
-There is also a very good Go plugin for intellij ("https://github.com/go-lang-plugin-org/go-lang-idea-plugin")
-GoJetPlugin + Go-lang-idea-plugin = Happiness :D
+The last release of v1 was v1.2 which is available at https://github.com/CloudyKit/jet/releases/tag/v1.2 and the tag v1.2.
-### Examples
-
-#### Simple
+To upgrade to v2 a few updates to your templates are necessary – these are explained in the [upgrade guide](https://github.com/CloudyKit/jet/wiki/Upgrade-to-v2).
-```HTML
+#### IntelliJ Plugin
-Hey {{name}}!
-No Escape {{ "Link " |unsafe}}
+If you use IntelliJ there is a plugin available at https://github.com/jhsx/GoJetPlugin.
+There is also a very good Go plugin for IntelliJ – see https://github.com/go-lang-plugin-org/go-lang-idea-plugin.
+GoJetPlugin + Go-lang-idea-plugin = happiness!
-```
-
-#### Extends
+### Examples
-```HTML
-{{extends "common/layout.jet"}}
+You can find examples in the [wiki](https://github.com/CloudyKit/jet/wiki/Jet-template-syntax).
+### Running the example application
-{{block Content}}
- {{.PageHeader}}
- {* this is a comment *}
- {{.PageContent |unsafe}}
-{{end}}
+An example application is available in the repository. Use `go get -u github.com/CloudyKit/jet` or clone the repository into `$GOPATH/github.com/CloudyKit/jet`, then do:
```
-
-
-#### Range
-
-```HTML
-{{extends "common/layout.jet"}}
-
-
-{{block Content}}
- {{.PageHeader}}
-
- {{range .Result}}
-
- {{end}}
-{{end}}
+ $ cd example; go run main.go
```
-#### Import Extends Yield
-
-```HTML
-{{extends "common/layout.jet"}}
-{{import "common/menu.jet"}}
-
-{{block Header}}
-
-{{yield menu}}
-{{end}}
-
-{{block Content}}
- {{.PageHeader}}
-
- {{range .Result}}
-
- {{end}}
-{{end}}
-```
#### Faster than some pre-compiled template engines
-Benchmark consist of range over a slice of data printing the values,
-the benchmark is based on "https://github.com/SlinSo/goTemplateBenchmark",
-
-Jet performs better than all template engines without pre-compilation, and peforms better than gorazor,
-Ftmpl, Egon which are pre-compiled to go.
-
+The benchmark consists of a range over a slice of data printing the values, the benchmark is based on https://github.com/SlinSo/goTemplateBenchmark, Jet performs better than all template engines without pre-compilation,
+and performs better than gorazor, Ftmpl and Egon, all of which are pre-compiled to Go.
###### Benchmarks
@@ -151,9 +97,9 @@ ok github.com/SlinSo/goTemplateBenchmark 36.200s
#### Contributing
-Any contribution is welcome, if you find a bug please report.
+All contributions are welcome – if you find a bug please report it.
#### Thanks
-- @golang developers, for the awesome language, and std library
-- @SlinSo for the benchmarks that i used as base to show the results above
+- @golang developers for the awesome language and the standard library
+- @SlinSo for the benchmarks that I used as a base to show the results above
diff --git a/constructors.go b/constructors.go
index fb39f69..4ca5559 100644
--- a/constructors.go
+++ b/constructors.go
@@ -31,12 +31,8 @@ func (t *Template) newTernaryExpr(pos Pos, line int, boolean, left, right Expres
return &TernaryExprNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeTernaryExpr, Pos: pos, Line: line}, Boolean: boolean, Left: left, Right: right}
}
-func (t *Template) newBuiltinExpr(pos Pos, line int, name string, nodetype NodeType) *BuiltinExprNode {
- return &BuiltinExprNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: nodetype, Pos: pos, Line: line}, Name: name}
-}
-
-func (t *Template) newSet(pos Pos, line int, isLet bool, left, right []Expression) *SetNode {
- return &SetNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeSet, Pos: pos, Line: line}, Let: isLet, Left: left, Right: right}
+func (t *Template) newSet(pos Pos, line int, isLet, isIndexExprGetLookup bool, left, right []Expression) *SetNode {
+ return &SetNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeSet, Pos: pos, Line: line}, Let: isLet, IndexExprGetLookup: isIndexExprGetLookup, Left: left, Right: right}
}
func (t *Template) newCallExpr(pos Pos, line int, expr Expression) *CallExprNode {
@@ -90,7 +86,7 @@ func (t *Template) newNil(pos Pos) *NilNode {
}
func (t *Template) newField(pos Pos, ident string) *FieldNode {
- return &FieldNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeField, Pos: pos}, Ident: strings.Split(ident[1:], ".")} // [1:] to drop leading period
+ return &FieldNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeField, Pos: pos}, Ident: strings.Split(ident[1:], ".")} //[1:] to drop leading period
}
func (t *Template) newChain(pos Pos, node Node) *ChainNode {
@@ -109,6 +105,10 @@ func (t *Template) newEnd(pos Pos) *endNode {
return &endNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: nodeEnd, Pos: pos}}
}
+func (t *Template) newContent(pos Pos) *contentNode {
+ return &contentNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: nodeContent, Pos: pos}}
+}
+
func (t *Template) newElse(pos Pos, line int) *elseNode {
return &elseNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: nodeElse, Pos: pos, Line: line}}
}
@@ -121,12 +121,12 @@ func (t *Template) newRange(pos Pos, line int, set *SetNode, pipe Expression, li
return &RangeNode{BranchNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeRange, Pos: pos, Line: line}, Set: set, Expression: pipe, List: list, ElseList: elseList}}
}
-func (t *Template) newBlock(pos Pos, line int, name string, pipe Expression, listNode *ListNode) *BlockNode {
- return &BlockNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeBlock, Line: line, Pos: pos}, Name: name, Expression: pipe, List: listNode}
+func (t *Template) newBlock(pos Pos, line int, name string, parameters *BlockParameterList, pipe Expression, listNode, contentListNode *ListNode) *BlockNode {
+ return &BlockNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeBlock, Line: line, Pos: pos}, Name: name, Parameters: parameters, Expression: pipe, List: listNode, Content: contentListNode}
}
-func (t *Template) newYield(pos Pos, line int, name string, pipe Expression) *YieldNode {
- return &YieldNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeYield, Pos: pos, Line: line}, Name: name, Expression: pipe}
+func (t *Template) newYield(pos Pos, line int, name string, bplist *BlockParameterList, pipe Expression, content *ListNode, isContent bool) *YieldNode {
+ return &YieldNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeYield, Pos: pos, Line: line}, Name: name, Parameters: bplist, Expression: pipe, Content: content, IsContent: isContent}
}
func (t *Template) newInclude(pos Pos, line int, name, pipe Expression) *IncludeNode {
@@ -137,22 +137,22 @@ func (t *Template) newNumber(pos Pos, text string, typ itemType) (*NumberNode, e
n := &NumberNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeNumber, Pos: pos}, Text: text}
switch typ {
case itemCharConstant:
- rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0])
+ _rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0])
if err != nil {
return nil, err
}
if tail != "'" {
return nil, fmt.Errorf("malformed character constant: %s", text)
}
- n.Int64 = int64(rune)
+ n.Int64 = int64(_rune)
n.IsInt = true
- n.Uint64 = uint64(rune)
+ n.Uint64 = uint64(_rune)
n.IsUint = true
- n.Float64 = float64(rune) // odd but those are the rules.
+ n.Float64 = float64(_rune) //odd but those are the rules.
n.IsFloat = true
return n, nil
case itemComplex:
- // fmt.Sscan can parse the pair, so let it do the work.
+ //fmt.Sscan can parse the pair, so let it do the work.
if _, err := fmt.Sscan(text, &n.Complex128); err != nil {
return nil, err
}
@@ -160,7 +160,7 @@ func (t *Template) newNumber(pos Pos, text string, typ itemType) (*NumberNode, e
n.simplifyComplex()
return n, nil
}
- // Imaginary constants can only be complex unless they are zero.
+ //Imaginary constants can only be complex unless they are zero.
if len(text) > 0 && text[len(text)-1] == 'i' {
f, err := strconv.ParseFloat(text[:len(text)-1], 64)
if err == nil {
diff --git a/default.go b/default.go
index aa74667..5435787 100644
--- a/default.go
+++ b/default.go
@@ -24,23 +24,62 @@ import (
"text/template"
)
-var defaultVariables = map[string]reflect.Value{
- "lower": reflect.ValueOf(strings.ToLower),
- "upper": reflect.ValueOf(strings.ToUpper),
- "hasPrefix": reflect.ValueOf(strings.HasPrefix),
- "hasSuffix": reflect.ValueOf(strings.HasSuffix),
- "repeat": reflect.ValueOf(strings.Repeat),
- "replace": reflect.ValueOf(strings.Replace),
- "split": reflect.ValueOf(strings.Split),
- "trimSpace": reflect.ValueOf(strings.TrimSpace),
- "map": reflect.ValueOf(newMap),
- "html": reflect.ValueOf(html.EscapeString),
- "url": reflect.ValueOf(url.QueryEscape),
- "safeHtml": reflect.ValueOf(SafeWriter(template.HTMLEscape)),
- "safeJs": reflect.ValueOf(SafeWriter(template.JSEscape)),
- "unsafe": reflect.ValueOf(SafeWriter(unsafePrinter)),
- "writeJson": reflect.ValueOf(jsonRenderer),
- "json": reflect.ValueOf(json.Marshal),
+var defaultExtensions = []string{
+ ".html.jet",
+ ".jet.html",
+ ".jet",
+}
+
+var defaultVariables map[string]reflect.Value
+
+func init() {
+ defaultVariables = map[string]reflect.Value{
+ "lower": reflect.ValueOf(strings.ToLower),
+ "upper": reflect.ValueOf(strings.ToUpper),
+ "hasPrefix": reflect.ValueOf(strings.HasPrefix),
+ "hasSuffix": reflect.ValueOf(strings.HasSuffix),
+ "repeat": reflect.ValueOf(strings.Repeat),
+ "replace": reflect.ValueOf(strings.Replace),
+ "split": reflect.ValueOf(strings.Split),
+ "trimSpace": reflect.ValueOf(strings.TrimSpace),
+ "map": reflect.ValueOf(newMap),
+ "html": reflect.ValueOf(html.EscapeString),
+ "url": reflect.ValueOf(url.QueryEscape),
+ "safeHtml": reflect.ValueOf(SafeWriter(template.HTMLEscape)),
+ "safeJs": reflect.ValueOf(SafeWriter(template.JSEscape)),
+ "raw": reflect.ValueOf(SafeWriter(unsafePrinter)),
+ "unsafe": reflect.ValueOf(SafeWriter(unsafePrinter)),
+ "writeJson": reflect.ValueOf(jsonRenderer),
+ "json": reflect.ValueOf(json.Marshal),
+ "isset": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
+ a.RequireNumOfArguments("isset", 1, 99999999999)
+ for i := 0; i < len(a.argExpr); i++ {
+ if !a.runtime.isSet(a.argExpr[i]) {
+ return valueBoolFALSE
+ }
+ }
+ return valueBoolTRUE
+ })),
+ "len": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
+ a.RequireNumOfArguments("len", 1, 1)
+
+ expression := a.Get(0)
+ if expression.Kind() == reflect.Ptr {
+ expression = expression.Elem()
+ }
+
+ switch expression.Kind() {
+ case reflect.Array, reflect.Chan, reflect.Slice, reflect.Map, reflect.String:
+ return reflect.ValueOf(expression.Len())
+ case reflect.Struct:
+ return reflect.ValueOf(expression.NumField())
+ }
+
+ a.Panicf("inválid value type %s in len builtin", expression.Type())
+ return reflect.Value{}
+ })),
+ }
+
}
func jsonRenderer(v interface{}) RendererFunc {
@@ -56,6 +95,8 @@ func unsafePrinter(w io.Writer, b []byte) {
w.Write(b)
}
+// SafeWriter escapee func. Functions implementing this type will write directly into the writer,
+// skipping the escape phase; use this type to create special types of escapee funcs.
type SafeWriter func(io.Writer, []byte)
func newMap(values ...interface{}) (nmap map[string]interface{}) {
diff --git a/eval.go b/eval.go
index cf41cdc..e5064df 100644
--- a/eval.go
+++ b/eval.go
@@ -19,10 +19,12 @@ import (
"io"
"reflect"
"runtime"
+ "strconv"
"sync"
)
var (
+ funcType = reflect.TypeOf(Func(nil))
stringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
rangerType = reflect.TypeOf((*Ranger)(nil)).Elem()
rendererType = reflect.TypeOf((*Renderer)(nil)).Elem()
@@ -34,16 +36,22 @@ var (
}
)
+// Renderer any resulting value from an expression in an action that implements this
+// interface will not be printed, instead, we will invoke his Render() method which will be responsible
+// to render his self
type Renderer interface {
Render(*Runtime)
}
+// RendererFunc func implementing interface Renderer
type RendererFunc func(*Runtime)
func (renderer RendererFunc) Render(r *Runtime) {
renderer(r)
}
+// Ranger a value implementing a ranger interface is able to iterate on his value
+// and can be used directly in a range statement
type Ranger interface {
Range() (reflect.Value, reflect.Value, bool)
}
@@ -63,10 +71,14 @@ func (w *escapeeWriter) Write(b []byte) (int, error) {
return 0, nil
}
+// Runtime this type holds the state of the execution of an template
type Runtime struct {
*escapeeWriter
*scope
- context reflect.Value
+ content func(*Runtime, Expression)
+
+ translator Translator
+ context reflect.Value
}
func (st *Runtime) newScope() {
@@ -110,9 +122,10 @@ func (st *scope) getBlock(name string) (block *BlockNode, has bool) {
return
}
+// YieldTemplate yields a template same as include
func (st Runtime) YieldTemplate(name string, context interface{}) {
- t, exists := st.set.getTemplate(name)
- if !exists {
+ t, err := st.set.GetTemplate(name)
+ if err != nil {
panic(fmt.Errorf("include: template %q was not found", name))
}
@@ -128,6 +141,7 @@ func (st Runtime) YieldTemplate(name string, context interface{}) {
st.executeList(Root)
}
+// Set sets variable ${name} in the current template scope
func (state *Runtime) Set(name string, val interface{}) {
state.setValue(name, reflect.ValueOf(val))
}
@@ -161,6 +175,7 @@ func (state *Runtime) setValue(name string, val reflect.Value) bool {
return true
}
+// Resolve resolves a value from the execution context
func (state *Runtime) Resolve(name string) reflect.Value {
if name == "." {
@@ -190,7 +205,6 @@ func (state *Runtime) Resolve(name string) reflect.Value {
}
func (st *Runtime) recover(err *error) {
- isDev := st.set.developmentMode
pool_State.Put(st)
if recovered := recover(); recovered != nil {
var is bool
@@ -198,7 +212,7 @@ func (st *Runtime) recover(err *error) {
panic(recovered)
}
*err, is = recovered.(error)
- if isDev || !is {
+ if !is {
panic(recovered)
}
}
@@ -245,22 +259,107 @@ RESTART:
}
func (st *Runtime) executeSetList(set *SetNode) {
- for i := 0; i < len(set.Left); i++ {
- st.executeSet(set.Left[i], st.evalPrimaryExpressionGroup(set.Right[i]))
+ if set.IndexExprGetLookup {
+ value := st.evalPrimaryExpressionGroup(set.Right[0])
+ st.executeSet(set.Left[0], value)
+ if value.IsValid() {
+ st.executeSet(set.Left[1], valueBoolTRUE)
+ } else {
+ st.executeSet(set.Left[1], valueBoolFALSE)
+ }
+ } else {
+ for i := 0; i < len(set.Left); i++ {
+ st.executeSet(set.Left[i], st.evalPrimaryExpressionGroup(set.Right[i]))
+ }
}
}
func (st *Runtime) executeLetList(set *SetNode) {
- for i := 0; i < len(set.Left); i++ {
- st.variables[set.Left[i].(*IdentifierNode).Ident] = st.evalPrimaryExpressionGroup(set.Right[i])
+ if set.IndexExprGetLookup {
+ value := st.evalPrimaryExpressionGroup(set.Right[0])
+
+ st.variables[set.Left[0].(*IdentifierNode).Ident] = value
+
+ if value.IsValid() {
+ st.variables[set.Left[1].(*IdentifierNode).Ident] = valueBoolTRUE
+ } else {
+ st.variables[set.Left[1].(*IdentifierNode).Ident] = valueBoolFALSE
+ }
+
+ } else {
+ for i := 0; i < len(set.Left); i++ {
+ st.variables[set.Left[i].(*IdentifierNode).Ident] = st.evalPrimaryExpressionGroup(set.Right[i])
+ }
+ }
+}
+
+func (st *Runtime) executeYieldBlock(block *BlockNode, blockParam, yieldParam *BlockParameterList, expression Expression, content *ListNode) {
+
+ needNewScope := len(blockParam.List) > 0 || len(yieldParam.List) > 0
+ if needNewScope {
+ st.newScope()
+ for i := 0; i < len(yieldParam.List); i++ {
+ p := &yieldParam.List[i]
+ st.variables[p.Identifier] = st.evalPrimaryExpressionGroup(p.Expression)
+ }
+ for i := 0; i < len(blockParam.List); i++ {
+ p := &blockParam.List[i]
+ if _, found := st.variables[p.Identifier]; !found {
+ if p.Expression == nil {
+ st.variables[p.Identifier] = valueBoolFALSE
+ } else {
+ st.variables[p.Identifier] = st.evalPrimaryExpressionGroup(p.Expression)
+ }
+ }
+ }
+ }
+
+ mycontent := st.content
+ if content != nil {
+ myscope := st.scope
+ st.content = func(st *Runtime, expression Expression) {
+ outscope := st.scope
+ outcontent := st.content
+
+ st.scope = myscope
+ st.content = mycontent
+
+ if expression != nil {
+ context := st.context
+ st.context = st.evalPrimaryExpressionGroup(expression)
+ st.executeList(content)
+ st.context = context
+ } else {
+ st.executeList(content)
+ }
+
+ st.scope = outscope
+ st.content = outcontent
+ }
+ }
+
+ if expression != nil {
+ context := st.context
+ st.context = st.evalPrimaryExpressionGroup(expression)
+ st.executeList(block.List)
+ st.context = context
+ } else {
+ st.executeList(block.List)
+ }
+
+ st.content = mycontent
+ if needNewScope {
+ st.releaseScope()
}
}
func (st *Runtime) executeList(list *ListNode) {
inNewSCOPE := false
+
for i := 0; i < len(list.Nodes); i++ {
node := list.Nodes[i]
switch node.Type() {
+
case NodeText:
node := node.(*TextNode)
_, err := st.Writer.Write(node.Text)
@@ -370,19 +469,16 @@ func (st *Runtime) executeList(list *ListNode) {
}
case NodeYield:
node := node.(*YieldNode)
- block, has := st.getBlock(node.Name)
-
- if has == false || block == nil {
- node.errorf("unresolved block %q!!", node.Name)
- }
-
- if node.Expression != nil {
- context := st.context
- st.context = st.evalPrimaryExpressionGroup(node.Expression)
- st.executeList(block.List)
- st.context = context
+ if node.IsContent {
+ if st.content != nil {
+ st.content(st, node.Expression)
+ }
} else {
- st.executeList(block.List)
+ block, has := st.getBlock(node.Name)
+ if has == false || block == nil {
+ node.errorf("unresolved block %q!!", node.Name)
+ }
+ st.executeYieldBlock(block, block.Parameters, node.Parameters, node.Expression, node.Content)
}
case NodeBlock:
node := node.(*BlockNode)
@@ -390,14 +486,7 @@ func (st *Runtime) executeList(list *ListNode) {
if has == false {
block = node
}
- if node.Expression != nil {
- context := st.context
- st.context = st.evalPrimaryExpressionGroup(node.Expression)
- st.executeList(block.List)
- st.context = context
- } else {
- st.executeList(block.List)
- }
+ st.executeYieldBlock(block, block.Parameters, block.Parameters, block.Expression, block.Content)
case NodeInclude:
node := node.(*IncludeNode)
var Name string
@@ -411,9 +500,9 @@ func (st *Runtime) executeList(list *ListNode) {
node.errorf("unexpected expression type %q in template yielding", getTypeString(name))
}
- t, exists := st.set.getTemplate(Name)
- if !exists {
- node.errorf("template %q was not found!!", node.Name)
+ t, err := st.set.getTemplate(Name, node.TemplateName)
+ if err != nil {
+ node.error(err)
} else {
st.newScope()
st.blocks = t.processedBlocks
@@ -471,22 +560,7 @@ func (st *Runtime) evalPrimaryExpressionGroup(node Expression) reflect.Value {
if baseExpr.Kind() != reflect.Func {
node.errorf("node %q is not func", node)
}
- return st.evalCallExpression(baseExpr, node.Args)[0]
- case NodeLenExpr:
- node := node.(*BuiltinExprNode)
- expression := st.evalPrimaryExpressionGroup(node.Args[0])
-
- if expression.Kind() == reflect.Ptr {
- expression = expression.Elem()
- }
-
- switch expression.Kind() {
- case reflect.Array, reflect.Chan, reflect.Slice, reflect.Map, reflect.String:
- return reflect.ValueOf(expression.Len())
- case reflect.Struct:
- return reflect.ValueOf(expression.NumField())
- }
- node.errorf("inválid value type %s in len builtin", expression.Type())
+ return st.evalCallExpression(baseExpr, node.Args)
case NodeIndexExpr:
node := node.(*IndexExprNode)
@@ -552,30 +626,21 @@ func (st *Runtime) evalPrimaryExpressionGroup(node Expression) reflect.Value {
}
return baseExpression.Slice(index, length)
-
- case NodeIssetExpr:
- node := node.(*BuiltinExprNode)
- for i := 0; i < len(node.Args); i++ {
- if !st.Isset(node.Args[i]) {
- return valueBoolFALSE
- }
- }
- return valueBoolTRUE
}
return st.evalBaseExpressionGroup(node)
}
-func (st *Runtime) Isset(node Node) bool {
+func (st *Runtime) isSet(node Node) bool {
nodeType := node.Type()
switch nodeType {
case NodeIndexExpr:
node := node.(*IndexExprNode)
- if !st.Isset(node.Base) {
+ if !st.isSet(node.Base) {
return false
}
- if !st.Isset(node.Index) {
+ if !st.isSet(node.Index) {
return false
}
@@ -770,6 +835,17 @@ func toInt(v reflect.Value) int64 {
return int64(v.Float())
} else if isUint(kind) {
return int64(v.Uint())
+ } else if kind == reflect.String {
+ n, e := strconv.ParseInt(v.String(), 10, 0)
+ if e != nil {
+ panic(e)
+ }
+ return n
+ } else if kind == reflect.Bool {
+ if v.Bool() {
+ return 0
+ }
+ return 1
}
panic(fmt.Errorf("type: %q can't be converted to int64", v.Type()))
return 0
@@ -779,12 +855,21 @@ func toUint(v reflect.Value) uint64 {
kind := v.Kind()
if isUint(kind) {
return v.Uint()
- }
- if isInt(kind) {
+ } else if isInt(kind) {
return uint64(v.Int())
- }
- if isFloat(kind) {
+ } else if isFloat(kind) {
return uint64(v.Float())
+ } else if kind == reflect.String {
+ n, e := strconv.ParseUint(v.String(), 10, 0)
+ if e != nil {
+ panic(e)
+ }
+ return n
+ } else if kind == reflect.Bool {
+ if v.Bool() {
+ return 0
+ }
+ return 1
}
panic(fmt.Errorf("type: %q can't be converted to uint64", v.Type()))
return 0
@@ -794,12 +879,21 @@ func toFloat(v reflect.Value) float64 {
kind := v.Kind()
if isFloat(kind) {
return v.Float()
- }
- if isInt(kind) {
+ } else if isInt(kind) {
return float64(v.Int())
- }
- if isUint(kind) {
+ } else if isUint(kind) {
return float64(v.Uint())
+ } else if kind == reflect.String {
+ n, e := strconv.ParseFloat(v.String(), 0)
+ if e != nil {
+ panic(e)
+ }
+ return n
+ } else if kind == reflect.Bool {
+ if v.Bool() {
+ return 0
+ }
+ return 1
}
panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
return 0
@@ -906,6 +1000,12 @@ func (st *Runtime) evalAdditiveExpression(node *AdditiveExprNode) reflect.Value
} else {
left = reflect.ValueOf(left.Uint() - toUint(right))
}
+ } else if kind == reflect.String {
+ if isAdditive {
+ left = reflect.ValueOf(left.String() + fmt.Sprint(right))
+ } else {
+ node.Right.errorf("minus signal is not allowed with strings")
+ }
} else {
node.Left.errorf("a non numeric value in additive expression")
}
@@ -976,12 +1076,25 @@ func (st *Runtime) evalBaseExpressionGroup(node Node) reflect.Value {
return reflect.Value{}
}
-func (st *Runtime) evalCallExpression(fn reflect.Value, args []Expression, values ...reflect.Value) []reflect.Value {
+func (st *Runtime) evalCallExpression(baseExpr reflect.Value, args []Expression, values ...reflect.Value) reflect.Value {
+
+ if funcType.AssignableTo(baseExpr.Type()) {
+ return baseExpr.Interface().(Func)(Arguments{runtime: st, argExpr: args, argVal: values})
+ }
+
i := len(args) + len(values)
+ var returns []reflect.Value
if i <= 10 {
- return reflect_Call10(i, st, fn, args, values...)
+ returns = reflect_Call10(i, st, baseExpr, args, values...)
+ } else {
+ returns = reflect_Call(make([]reflect.Value, i, i), st, baseExpr, args, values...)
+ }
+
+ if len(returns) == 0 {
+ return reflect.Value{}
}
- return reflect_Call(make([]reflect.Value, i, i), st, fn, args, values...)
+
+ return returns[0]
}
func (st *Runtime) evalCommandExpression(node *CommandNode) (reflect.Value, bool) {
@@ -992,11 +1105,7 @@ func (st *Runtime) evalCommandExpression(node *CommandNode) (reflect.Value, bool
st.evalSafeWriter(term, node)
return reflect.Value{}, true
}
- returned := st.evalCallExpression(term, node.Args)
- if len(returned) == 0 {
- return reflect.Value{}, false
- }
- return returned[0], false
+ return st.evalCallExpression(term, node.Args), false
} else {
node.Args[0].errorf("command %q type %s is not func", node.Args[0], term.Type())
}
@@ -1015,7 +1124,7 @@ func (w *escapeWriter) Write(b []byte) (int, error) {
}
func (st *Runtime) evalSafeWriter(term reflect.Value, node *CommandNode, v ...reflect.Value) {
- //todo: sync.Pool ?
+
sw := &escapeWriter{rawWriter: st.Writer, safeWriter: term.Interface().(SafeWriter)}
for i := 0; i < len(v); i++ {
fastprinter.PrintValue(sw, v[i])
@@ -1032,11 +1141,7 @@ func (st *Runtime) evalCommandPipeExpression(node *CommandNode, value reflect.Va
st.evalSafeWriter(term, node, value)
return reflect.Value{}, true
}
- returned := st.evalCallExpression(term, node.Args, value)
- if len(returned) == 0 {
- return reflect.Value{}, false
- }
- return returned[0], false
+ return st.evalCallExpression(term, node.Args, value), false
} else {
node.BaseExpr.errorf("pipe command %q type %s is not func", node.BaseExpr, term.Type())
}
@@ -1121,7 +1226,7 @@ func isFloat(kind reflect.Kind) bool {
return kind == reflect.Float32 || kind == reflect.Float64
}
-//checkEquality of two reflect values in the semantic of the jet runtime
+// checkEquality of two reflect values in the semantic of the jet runtime
func checkEquality(v1, v2 reflect.Value) bool {
if !v1.IsValid() || !v2.IsValid() {
@@ -1426,7 +1531,7 @@ type chanRanger struct {
v reflect.Value
}
-func (s *chanRanger) Range() (index, value reflect.Value, end bool) {
+func (s *chanRanger) Range() (_, value reflect.Value, end bool) {
value, end = s.v.Recv()
if end {
pool_chanRanger.Put(s)
diff --git a/eval_test.go b/eval_test.go
index ac9fe82..07ce1f6 100644
--- a/eval_test.go
+++ b/eval_test.go
@@ -17,54 +17,128 @@ import (
"bytes"
"fmt"
"io"
+ "reflect"
+ "runtime"
"strings"
+ "sync"
"testing"
"text/template"
)
-var evalTemplateSet = NewSet()
+var (
+ JetTestingSet = NewSet(nil)
-func evalTestCase(t *testing.T, variables VarMap, context interface{}, testName, testContent, testExpected string) {
- buff := bytes.NewBuffer(nil)
-
- tt, err := evalTemplateSet.loadTemplate(testName, testContent)
- if err != nil {
- t.Errorf("Parsing error: %s %s %s", err.Error(), testName, testContent)
- return
+ ww io.Writer = (*devNull)(nil)
+ users = []*User{
+ {"Mario Santos", "mario@gmail.com"},
+ {"Joel Silva", "joelsilva@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Mario Santos", "mario@gmail.com"},
+ {"Joel Silva", "joelsilva@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Mario Santos", "mario@gmail.com"},
+ {"Joel Silva", "joelsilva@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Mario Santos", "mario@gmail.com"},
+ {"Joel Silva", "joelsilva@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Mario Santos", "mario@gmail.com"},
+ {"Joel Silva", "joelsilva@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Mario Santos", "mario@gmail.com"},
+ {"Joel Silva", "joelsilva@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
+ {"Luis Santana", "luis.santana@gmail.com"},
}
- err = tt.Execute(buff, variables, context)
+ stdSet = template.New("base")
+)
+
+type devNull struct{}
+
+func (*devNull) Write(_ []byte) (int, error) {
+ return 0, nil
+}
+
+func dummy(a string) string {
+ return a
+}
+
+func init() {
+ stdSet.Funcs(template.FuncMap{"dummy": dummy})
+ _, err := stdSet.Parse(`
+ {{define "actionNode_dummy"}}hello {{dummy "WORLD"}}{{end}}
+ {{define "noAllocFn"}}hello {{ "José" }} {{1}} {{ "José" }} {{end}}
+ {{define "rangeOverUsers_Set"}}{{range $index,$val := . }}{{$index}}:{{$val.Name}}-{{$val.Email}}{{end}}{{end}}
+ {{define "rangeOverUsers"}}{{range . }}{{.Name}}-{{.Email}}{{end}}{{end}}
+ `)
if err != nil {
- t.Errorf("Eval error: %q executing %s", err.Error(), testName)
- return
+ println(err.Error())
}
- result := buff.String()
- if result != testExpected {
- t.Errorf("Result error: %q expected, got %q on %s", testExpected, result, testName)
- }
+ JetTestingSet.AddGlobal("dummy", dummy)
+ JetTestingSet.LoadTemplate("actionNode_dummy", `hello {{dummy("WORLD")}}`)
+ JetTestingSet.LoadTemplate("noAllocFn", `hello {{ "José" }} {{1}} {{ "José" }}`)
+ JetTestingSet.LoadTemplate("rangeOverUsers", `{{range .}}{{.Name}}-{{.Email}}{{end}}`)
+ JetTestingSet.LoadTemplate("rangeOverUsers_Set", `{{range index,user:= . }}{{index}}{{user.Name}}-{{user.Email}}{{end}}`)
+
+ JetTestingSet.LoadTemplate("BenchNewBlock", "{{ block col(md=12,offset=0) }}\n{{ yield content }}
\n\t\t{{ end }}\n\t\t{{ block row(md=12) }}\n{{ yield content }}
\n\t\t{{ content }}\n
\n
\n
\n\t\t{{ end }}\n\t\t{{ block header() }}\n\n\t\t{{content}}\nHey \n\t\t{{ end }}")
}
-func evalTestCaseSet(testingSet *Set, t *testing.T, variables VarMap, context interface{}, testName, testContent, testExpected string) {
- buff := bytes.NewBuffer(nil)
- tt, err := testingSet.loadTemplate(testName, testContent)
- if err != nil {
- t.Errorf("Parsing error: %s %s %s", err.Error(), testName, testContent)
- return
+func RunJetTest(t *testing.T, variables VarMap, context interface{}, testName, testContent, testExpected string) {
+ RunJetTestWithSet(t, JetTestingSet, variables, context, testName, testContent, testExpected)
+}
+
+func RunJetTestWithSet(t *testing.T, set *Set, variables VarMap, context interface{}, testName, testContent, testExpected string) {
+ var (
+ tt *Template
+ err error
+ )
+
+ if testContent != "" {
+ tt, err = set.LoadTemplate(testName, testContent)
+ } else {
+ tt, err = set.GetTemplate(testName)
}
- err = tt.Execute(buff, variables, context)
+
if err != nil {
- t.Errorf("Eval error: %q executing %s", err.Error(), testName)
+ t.Errorf("Parsing error: %s %s %s", err.Error(), testName, testContent)
return
}
- result := buff.String()
- if result != testExpected {
- t.Errorf("Result error expected %q got %q on %s", testExpected, result, testName)
+ RunJetTestWithTemplate(t, tt, variables, context, testExpected)
+}
+
+func RunJetTestWithTemplate(t *testing.T, tt *Template, variables VarMap, context interface{}, testExpected string) {
+ if testing.RunTests(func(pat, str string) (bool, error) {
+ return true, nil
+ }, []testing.InternalTest{
+ {
+ Name: fmt.Sprintf("\tJetTest(%s)", tt.Name),
+ F: func(t *testing.T) {
+ buff := bytes.NewBuffer(nil)
+ err := tt.Execute(buff, variables, context)
+ if err != nil {
+ t.Errorf("Eval error: %q executing %s", err.Error(), tt.Name)
+ return
+ }
+ result := buff.String()
+ if result != testExpected {
+ t.Errorf("Result error expected %q got %q on %s", testExpected, result, tt.Name)
+ }
+ },
+ },
+ }) == false {
+ t.Fail()
}
}
func TestEvalTextNode(t *testing.T) {
- evalTestCase(t, nil, nil, "textNode", `hello {*Buddy*} World`, `hello World`)
+ RunJetTest(t, nil, nil, "textNode", `hello {*Buddy*} World`, `hello World`)
}
type User struct {
@@ -86,34 +160,40 @@ func TestEvalActionNode(t *testing.T) {
"José Santos", "email@example.com",
})
- evalTestCase(t, nil, nil, "actionNode", `hello {{"world"}}`, `hello world`)
- evalTestCase(t, data, nil, "actionNode_func", `hello {{lower: "WORLD"}}`, `hello world`)
- evalTestCase(t, data, nil, "actionNode_funcPipe", `hello {{lower: "WORLD" |upper}}`, `hello WORLD`)
- evalTestCase(t, data, nil, "actionNode_funcPipeArg", `hello {{lower: "WORLD-" |upper|repeat: 2}}`, `hello WORLD-WORLD-`)
- evalTestCase(t, data, nil, "actionNode_Field", `Oi {{ user.Name }}`, `Oi José Santos`)
- evalTestCase(t, data, nil, "actionNode_Field2", `Oi {{ user.Name }}<{{ user.Email }}>`, `Oi José Santos`)
- evalTestCase(t, data, nil, "actionNode_Method", `Oi {{ user.Format: "%s<%s>" }}`, `Oi José Santos`)
-
- evalTestCase(t, data, nil, "actionNode_Add", `{{ 2+1 }}`, fmt.Sprint(2+1))
- evalTestCase(t, data, nil, "actionNode_Add3", `{{ 2+1+4 }}`, fmt.Sprint(2+1+4))
- evalTestCase(t, data, nil, "actionNode_Add3Minus", `{{ 2+1+4-3 }}`, fmt.Sprint(2+1+4-3))
- evalTestCase(t, data, nil, "actionNode_Mult", `{{ 4*4 }}`, fmt.Sprint(4*4))
- evalTestCase(t, data, nil, "actionNode_MultAdd", `{{ 2+4*4 }}`, fmt.Sprint(2+4*4))
- evalTestCase(t, data, nil, "actionNode_MultAdd1", `{{ 4*2+4 }}`, fmt.Sprint(4*2+4))
- evalTestCase(t, data, nil, "actionNode_MultAdd2", `{{ 2+4*2+4 }}`, fmt.Sprint(2+4*2+4))
- evalTestCase(t, data, nil, "actionNode_MultFloat", `{{ 1.23*1 }}`, fmt.Sprint(1*1.23))
- evalTestCase(t, data, nil, "actionNode_Mod", `{{ 3%2 }}`, fmt.Sprint(3%2))
- evalTestCase(t, data, nil, "actionNode_MultMod", `{{ (1*3)%2 }}`, fmt.Sprint((1*3)%2))
- evalTestCase(t, data, nil, "actionNode_MultDivMod", `{{ (2*5)/ 3 %1 }}`, fmt.Sprint((2*5)/3%1))
-
- evalTestCase(t, data, nil, "actionNode_Comparation", `{{ (2*5)==10 }}`, fmt.Sprint((2*5) == 10))
- evalTestCase(t, data, nil, "actionNode_Comparatation2", `{{ (2*5)==5 }}`, fmt.Sprint((2*5) == 5))
- evalTestCase(t, data, nil, "actionNode_Logical", `{{ (2*5)==5 || true }}`, fmt.Sprint((2*5) == 5 || true))
- evalTestCase(t, data, nil, "actionNode_Logical2", `{{ (2*5)==5 || false }}`, fmt.Sprint((2*5) == 5 || false))
-
- evalTestCase(t, data, nil, "actionNode_NumericCmp", `{{ 5*5 > 2*12.5 }}`, fmt.Sprint(5*5 > 2*12.5))
- evalTestCase(t, data, nil, "actionNode_NumericCmp1", `{{ 5*5 >= 2*12.5 }}`, fmt.Sprint(5*5 >= 2*12.5))
- evalTestCase(t, data, nil, "actionNode_NumericCmp1", `{{ 5 * 5 > 2 * 12.5 == 5 * 5 > 2 * 12.5 }}`, fmt.Sprint((5*5 > 2*12.5) == (5*5 > 2*12.5)))
+ RunJetTest(t, nil, nil, "actionNode", `hello {{"world"}}`, `hello world`)
+ RunJetTest(t, data, nil, "actionNode_func", `hello {{lower: "WORLD"}}`, `hello world`)
+ RunJetTest(t, data, nil, "actionNode_funcPipe", `hello {{lower: "WORLD" |upper}}`, `hello WORLD`)
+ RunJetTest(t, data, nil, "actionNode_funcPipeArg", `hello {{lower: "WORLD-" |upper|repeat: 2}}`, `hello WORLD-WORLD-`)
+ RunJetTest(t, data, nil, "actionNode_Field", `Oi {{ user.Name }}`, `Oi José Santos`)
+ RunJetTest(t, data, nil, "actionNode_Field2", `Oi {{ user.Name }}<{{ user.Email }}>`, `Oi José Santos`)
+ RunJetTest(t, data, nil, "actionNode_Method", `Oi {{ user.Format: "%s<%s>" }}`, `Oi José Santos`)
+
+ RunJetTest(t, data, nil, "actionNode_Add", `{{ 2+1 }}`, fmt.Sprint(2+1))
+ RunJetTest(t, data, nil, "actionNode_Add3", `{{ 2+1+4 }}`, fmt.Sprint(2+1+4))
+ RunJetTest(t, data, nil, "actionNode_Add3Minus", `{{ 2+1+4-3 }}`, fmt.Sprint(2+1+4-3))
+
+ RunJetTest(t, data, nil, "actionNode_AddIntString", `{{ 2+"1" }}`, "3")
+ RunJetTest(t, data, nil, "actionNode_AddStringInt", `{{ "1"+2 }}`, "12")
+
+ //this is an error RunJetTest(t, data, nil, "actionNode_AddStringInt", `{{ "1"-2 }}`, "12")
+
+ RunJetTest(t, data, nil, "actionNode_Mult", `{{ 4*4 }}`, fmt.Sprint(4*4))
+ RunJetTest(t, data, nil, "actionNode_MultAdd", `{{ 2+4*4 }}`, fmt.Sprint(2+4*4))
+ RunJetTest(t, data, nil, "actionNode_MultAdd1", `{{ 4*2+4 }}`, fmt.Sprint(4*2+4))
+ RunJetTest(t, data, nil, "actionNode_MultAdd2", `{{ 2+4*2+4 }}`, fmt.Sprint(2+4*2+4))
+ RunJetTest(t, data, nil, "actionNode_MultFloat", `{{ 1.23*1 }}`, fmt.Sprint(1*1.23))
+ RunJetTest(t, data, nil, "actionNode_Mod", `{{ 3%2 }}`, fmt.Sprint(3%2))
+ RunJetTest(t, data, nil, "actionNode_MultMod", `{{ (1*3)%2 }}`, fmt.Sprint((1*3)%2))
+ RunJetTest(t, data, nil, "actionNode_MultDivMod", `{{ (2*5)/ 3 %1 }}`, fmt.Sprint((2*5)/3%1))
+
+ RunJetTest(t, data, nil, "actionNode_Comparation", `{{ (2*5)==10 }}`, fmt.Sprint((2*5) == 10))
+ RunJetTest(t, data, nil, "actionNode_Comparatation2", `{{ (2*5)==5 }}`, fmt.Sprint((2*5) == 5))
+ RunJetTest(t, data, nil, "actionNode_Logical", `{{ (2*5)==5 || true }}`, fmt.Sprint((2*5) == 5 || true))
+ RunJetTest(t, data, nil, "actionNode_Logical2", `{{ (2*5)==5 || false }}`, fmt.Sprint((2*5) == 5 || false))
+
+ RunJetTest(t, data, nil, "actionNode_NumericCmp", `{{ 5*5 > 2*12.5 }}`, fmt.Sprint(5*5 > 2*12.5))
+ RunJetTest(t, data, nil, "actionNode_NumericCmp1", `{{ 5*5 >= 2*12.5 }}`, fmt.Sprint(5*5 >= 2*12.5))
+ RunJetTest(t, data, nil, "actionNode_NumericCmp1", `{{ 5 * 5 > 2 * 12.5 == 5 * 5 > 2 * 12.5 }}`, fmt.Sprint((5*5 > 2*12.5) == (5*5 > 2*12.5)))
}
func TestEvalIfNode(t *testing.T) {
@@ -126,11 +206,11 @@ func TestEvalIfNode(t *testing.T) {
"José Santos", "email@example.com",
})
- evalTestCase(t, data, nil, "ifNode_simples", `{{if true}}hello{{end}}`, `hello`)
- evalTestCase(t, data, nil, "ifNode_else", `{{if false}}hello{{else}}world{{end}}`, `world`)
- evalTestCase(t, data, nil, "ifNode_elseif", `{{if false}}hello{{else if true}}world{{end}}`, `world`)
- evalTestCase(t, data, nil, "ifNode_elseif_else", `{{if false}}hello{{else if false}}world{{else}}buddy{{end}}`, `buddy`)
- evalTestCase(t, data, nil, "ifNode_string_comparison", `{{user.Name}} (email: {{user.Email}}): {{if user.Email == "email2@example.com"}}email is email2@example.com{{else}}email is not email2@example.com{{end}}`, `José Santos (email: email@example.com): email is not email2@example.com`)
+ RunJetTest(t, data, nil, "ifNode_simples", `{{if true}}hello{{end}}`, `hello`)
+ RunJetTest(t, data, nil, "ifNode_else", `{{if false}}hello{{else}}world{{end}}`, `world`)
+ RunJetTest(t, data, nil, "ifNode_elseif", `{{if false}}hello{{else if true}}world{{end}}`, `world`)
+ RunJetTest(t, data, nil, "ifNode_elseif_else", `{{if false}}hello{{else if false}}world{{else}}buddy{{end}}`, `buddy`)
+ RunJetTest(t, data, nil, "ifNode_string_comparison", `{{user.Name}} (email: {{user.Email}}): {{if user.Email == "email2@example.com"}}email is email2@example.com{{else}}email is not email2@example.com{{end}}`, `José Santos (email: email@example.com): email is not email2@example.com`)
}
@@ -141,13 +221,24 @@ func TestEvalBlockYieldIncludeNode(t *testing.T) {
"José Santos", "email@example.com",
})
- evalTestCase(t, data, nil, "Block_simple", `{{block hello "Buddy" }}Hello {{ . }}{{end}},{{yield hello user.Name}}`, `Hello Buddy,Hello José Santos`)
- evalTestCase(t, data, nil, "Block_Extends", `{{extends "Block_simple"}}{{block hello "Buddy" }}Hey {{ . }}{{end}}`, `Hey Buddy,Hey José Santos`)
- evalTestCase(t, data, nil, "Block_Import", `{{import "Block_simple"}}{{yield hello "Buddy"}}`, `Hello Buddy`)
- evalTestCase(t, data, nil, "Block_Import", `{{import "Block_simple"}}{{yield hello "Buddy"}}`, `Hello Buddy`)
+ RunJetTest(t, data, nil, "Block_simple", `{{block hello() "Buddy" }}Hello {{ . }}{{end}},{{yield hello() user.Name}}`, `Hello Buddy,Hello José Santos`)
+ RunJetTest(t, data, nil, "Block_Extends", `{{extends "Block_simple"}}{{block hello() "Buddy" }}Hey {{ . }}{{end}}`, `Hey Buddy,Hey José Santos`)
+ RunJetTest(t, data, nil, "Block_Import", `{{import "Block_simple"}}{{yield hello() "Buddy"}}`, `Hello Buddy`)
+ RunJetTest(t, data, nil, "Block_Import", `{{import "Block_simple"}}{{yield hello() "Buddy"}}`, `Hello Buddy`)
+
+ JetTestingSet.LoadTemplate("Block_ImportInclude1", `{{yield hello() "Buddy"}}`)
+ RunJetTest(t, data, nil, "Block_ImportInclude", `{{ import "Block_simple"}}{{include "Block_ImportInclude1"}}`, `Hello Buddy`)
+ RunJetTest(t, data, nil,
+ "Block_Content",
+ "{{ block col(md=12,offset=0) }}\n{{ yield content }}
\n\t\t{{ end }}\n\t\t{{ block row(md=12) }}\n{{ yield content }}
\n\t\t{{ content }}\n
\n
\n
\n\t\t{{ end }}\n\t\t{{ block header() }}\n\n\t\t{{content}}\nHey \n\t\t{{ end }}",
+ "\n
\n\t\t\n\t\t\n\n\t\t\n\t\t\n\n\t\t",
+ )
+
+ JetTestingSet.LoadTemplate("BlockContentLib", "{{block col(columns)}}\n {{yield content}}
\n{{end}}\n{{block row(cols=\"\")}}\n \n {{if len(cols) > 0}}\n {{yield col(columns=cols) content}}{{yield content}}{{end}}\n {{else}}\n {{yield content}}\n {{end}}\n
\n{{end}}")
+ RunJetTest(t, nil, nil, "BlockContentParam",
+ `{{import "BlockContentLib"}}{{yield row(cols="12") content}}{{cols}}{{end}}`,
+ "\n \n")
- evalTemplateSet.LoadTemplate("Block_ImportInclude1", `{{yield hello "Buddy"}}`)
- evalTestCase(t, data, nil, "Block_ImportInclude", `{{ import "Block_simple"}}{{include "Block_ImportInclude1"}}`, `Hello Buddy`)
}
func TestEvalRangeNode(t *testing.T) {
@@ -161,145 +252,113 @@ func TestEvalRangeNode(t *testing.T) {
})
const resultString = `Mario Santosmario@gmail.com Joel Silvajoelsilva@gmail.com Luis Santanaluis.santana@gmail.com `
- evalTestCase(t, data, nil, "Range_Expression", `{{range users}}{{.Name}}{{.Email}} {{end}}`, resultString)
- evalTestCase(t, data, nil, "Range_ExpressionValue", `{{range user:=users}}{{user.Name}}{{user.Email}} {{end}}`, resultString)
+ RunJetTest(t, data, nil, "Range_Expression", `{{range users}}{{.Name}}{{.Email}} {{end}}`, resultString)
+ RunJetTest(t, data, nil, "Range_ExpressionValue", `{{range user:=users}}{{user.Name}}{{user.Email}} {{end}}`, resultString)
var resultString2 = `0: Mario Santosmario@gmail.com Joel Silvajoelsilva@gmail.com 2: Luis Santanaluis.santana@gmail.com `
- evalTestCase(t, data, nil, "Range_ExpressionValueIf", `{{range i, user:=users}}{{if i == 0 || i == 2}}{{i}}: {{end}}{{user.Name}}{{user.Email}} {{end}}`, resultString2)
+ RunJetTest(t, data, nil, "Range_ExpressionValueIf", `{{range i, user:=users}}{{if i == 0 || i == 2}}{{i}}: {{end}}{{user.Name}}{{user.Email}} {{end}}`, resultString2)
}
func TestEvalDefaultFuncs(t *testing.T) {
- evalTestCase(t, nil, nil, "DefaultFuncs_safeHtml", `{{"Hello Buddy! " |safeHtml}} `, `<h1>Hello Buddy!</h1> `)
- evalTestCase(t, nil, nil, "DefaultFuncs_safeHtml2", `{{safeHtml: "Hello Buddy! "}} `, `<h1>Hello Buddy!</h1> `)
- evalTestCase(t, nil, nil, "DefaultFuncs_htmlEscape", `{{html: "Hello Buddy! "}} `, `<h1>Hello Buddy!</h1> `)
- evalTestCase(t, nil, nil, "DefaultFuncs_urlEscape", `{{url: "Hello Buddy! "}} `, `%3Ch1%3EHello+Buddy%21%3C%2Fh1%3E `)
+ RunJetTest(t, nil, nil, "DefaultFuncs_safeHtml", `{{"Hello Buddy! " |safeHtml}} `, `<h1>Hello Buddy!</h1> `)
+ RunJetTest(t, nil, nil, "DefaultFuncs_safeHtml2", `{{safeHtml: "Hello Buddy! "}} `, `<h1>Hello Buddy!</h1> `)
+ RunJetTest(t, nil, nil, "DefaultFuncs_htmlEscape", `{{html: "Hello Buddy! "}} `, `<h1>Hello Buddy!</h1> `)
+ RunJetTest(t, nil, nil, "DefaultFuncs_urlEscape", `{{url: "Hello Buddy! "}} `, `%3Ch1%3EHello+Buddy%21%3C%2Fh1%3E `)
- evalTestCase(t, nil, &User{"Mario Santos", "mario@gmail.com"}, "DefaultFuncs_json", `{{. |writeJson}}`, "{\"Name\":\"Mario Santos\",\"Email\":\"mario@gmail.com\"}\n")
+ RunJetTest(t, nil, &User{"Mario Santos", "mario@gmail.com"}, "DefaultFuncs_json", `{{. |writeJson}}`, "{\"Name\":\"Mario Santos\",\"Email\":\"mario@gmail.com\"}\n")
}
func TestEvalIssetAndTernaryExpression(t *testing.T) {
var data = make(VarMap)
data.Set("title", "title")
- evalTestCase(t, nil, nil, "IssetExpression_1", `{{isset(value)}}`, "false")
- evalTestCase(t, data, nil, "IssetExpression_2", `{{isset(title)}}`, "true")
+ RunJetTest(t, nil, nil, "IssetExpression_1", `{{isset(value)}}`, "false")
+ RunJetTest(t, data, nil, "IssetExpression_2", `{{isset(title)}}`, "true")
user := &User{
"José Santos", "email@example.com",
}
- evalTestCase(t, nil, user, "IssetExpression_3", `{{isset(.Name)}}`, "true")
- evalTestCase(t, nil, user, "IssetExpression_4", `{{isset(.Names)}}`, "false")
- evalTestCase(t, data, user, "IssetExpression_5", `{{isset(title)}}`, "true")
- evalTestCase(t, data, user, "IssetExpression_6", `{{isset(title.Get)}}`, "false")
+ RunJetTest(t, nil, user, "IssetExpression_3", `{{isset(.Name)}}`, "true")
+ RunJetTest(t, nil, user, "IssetExpression_4", `{{isset(.Names)}}`, "false")
+ RunJetTest(t, data, user, "IssetExpression_5", `{{isset(title)}}`, "true")
+ RunJetTest(t, data, user, "IssetExpression_6", `{{isset(title.Get)}}`, "false")
- evalTestCase(t, nil, user, "TernaryExpression_4", `{{isset(.Names)?"All names":"no names"}}`, "no names")
+ RunJetTest(t, nil, user, "TernaryExpression_4", `{{isset(.Names)?"All names":"no names"}}`, "no names")
- evalTestCase(t, nil, user, "TernaryExpression_5", `{{isset(.Name)?"All names":"no names"}}`, "All names")
- evalTestCase(t, data, user, "TernaryExpression_6", `{{ isset(form) ? form.Get("value") : "no form" }}`, "no form")
+ RunJetTest(t, nil, user, "TernaryExpression_5", `{{isset(.Name)?"All names":"no names"}}`, "All names")
+ RunJetTest(t, data, user, "TernaryExpression_6", `{{ isset(form) ? form.Get("value") : "no form" }}`, "no form")
}
func TestEvalIndexExpression(t *testing.T) {
- evalTestCase(t, nil, []string{"111", "222"}, "IndexExpressionSlice_1", `{{.[1]}}`, `222`)
- evalTestCase(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_1", `{{.["name"]}}`, "value")
- evalTestCase(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_2", `{{.["non_existant_key"]}}`, "")
- evalTestCase(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_3", `{{isset(.["non_existant_key"]) ? "key does exist" : "key does not exist"}}`, "key does not exist")
- //evalTestCase(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_4", `{{if v, ok := .["name"]; ok}}key does exist and has the value '{{v}}'{{else}}key does not exist{{end}}`, "key does exist and has the value 'value'")
- //evalTestCase(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_5", `{{if v, ok := .["non_existant_key"]; ok}}key does exist and has the value '{{v}}'{{else}}key does not exist{{end}}`, "key does not exist")
- evalTestCase(t, nil, &User{"José Santos", "email@example.com"}, "IndexExpressionStruct_1", `{{.[0]}}`, "José Santos")
- evalTestCase(t, nil, &User{"José Santos", "email@example.com"}, "IndexExpressionStruct_2", `{{.["Email"]}}`, "email@example.com")
+ RunJetTest(t, nil, []string{"111", "222"}, "IndexExpressionSlice_1", `{{.[1]}}`, `222`)
+ RunJetTest(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_1", `{{.["name"]}}`, "value")
+ RunJetTest(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_2", `{{.["non_existant_key"]}}`, "")
+ RunJetTest(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_3", `{{isset(.["non_existant_key"]) ? "key does exist" : "key does not exist"}}`, "key does not exist")
+ RunJetTest(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_4", `{{if v, ok := .["name"]; ok}}key does exist and has the value '{{v}}'{{else}}key does not exist{{end}}`, "key does exist and has the value 'value'")
+ RunJetTest(t, nil, map[string]string{"name": "value"}, "IndexExpressionMap_5", `{{if v, ok := .["non_existant_key"]; ok}}key does exist and has the value '{{v}}'{{else}}key does not exist{{end}}`, "key does not exist")
+ RunJetTest(t, nil, &User{"José Santos", "email@example.com"}, "IndexExpressionStruct_1", `{{.[0]}}`, "José Santos")
+ RunJetTest(t, nil, &User{"José Santos", "email@example.com"}, "IndexExpressionStruct_2", `{{.["Email"]}}`, "email@example.com")
}
func TestEvalSliceExpression(t *testing.T) {
- evalTestCase(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_1", `{{range .[1:]}}{{.}}{{end}}`, `222333444`)
- evalTestCase(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_2", `{{range .[:2]}}{{.}}{{end}}`, `111222`)
- evalTestCase(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_3", `{{range .[:]}}{{.}}{{end}}`, `111222333444`)
- evalTestCase(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_4", `{{range .[0:2]}}{{.}}{{end}}`, `111222`)
- evalTestCase(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_5", `{{range .[1:2]}}{{.}}{{end}}`, `222`)
- evalTestCase(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_6", `{{range .[1:3]}}{{.}}{{end}}`, `222333`)
-
- evalTestCase(t, nil, []string{"111"}, "SliceExpressionSlice_BugIndex", `{{range k,v:= . }}{{k}}{{end}}`, `0`)
+ RunJetTest(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_1", `{{range .[1:]}}{{.}}{{end}}`, `222333444`)
+ RunJetTest(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_2", `{{range .[:2]}}{{.}}{{end}}`, `111222`)
+ RunJetTest(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_3", `{{range .[:]}}{{.}}{{end}}`, `111222333444`)
+ RunJetTest(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_4", `{{range .[0:2]}}{{.}}{{end}}`, `111222`)
+ RunJetTest(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_5", `{{range .[1:2]}}{{.}}{{end}}`, `222`)
+ RunJetTest(t, nil, []string{"111", "222", "333", "444"}, "SliceExpressionSlice_6", `{{range .[1:3]}}{{.}}{{end}}`, `222333`)
+
+ RunJetTest(t, nil, []string{"111"}, "SliceExpressionSlice_BugIndex", `{{range k,v:= . }}{{k}}{{end}}`, `0`)
}
func TestEvalBuiltinExpression(t *testing.T) {
var data = make(VarMap)
- evalTestCase(t, data, nil, "LenExpression_1", `{{len("111")}}`, "3")
+ RunJetTest(t, data, nil, "LenExpression_1", `{{len("111")}}`, "3")
+ RunJetTest(t, data, nil, "LenExpression_2", `{{isset(data)?len(data):0}}`, "0")
+ RunJetTest(t, data, []string{"", "", "", ""}, "LenExpression_3", `{{len(.)}}`, "4")
}
func TestEvalAutoescape(t *testing.T) {
set := NewHTMLSet()
- evalTestCaseSet(set, t, nil, nil, "Autoescapee_Test1", `{{"Hello Buddy! " }} `, "<h1>Hello Buddy!</h1> ")
- evalTestCaseSet(set, t, nil, nil, "Autoescapee_Test2", `{{"Hello Buddy! " |unsafe }} `, "Hello Buddy! ")
+ RunJetTestWithSet(t, set, nil, nil, "Autoescapee_Test1", `{{"Hello Buddy! " }} `, "<h1>Hello Buddy!</h1> ")
+ RunJetTestWithSet(t, set, nil, nil, "Autoescapee_Test2", `{{"Hello Buddy! " |unsafe }} `, "Hello Buddy! ")
}
-func TestBugInGetValueWithPtrMethod(t *testing.T) {
- var data = make(VarMap)
-
- type ComplexType struct {
- User User
+func TestFileResolve(t *testing.T) {
+ set := NewHTMLSet("./testData/resolve")
+ RunJetTestWithSet(t, set, nil, nil, "simple", "", "simple")
+ RunJetTestWithSet(t, set, nil, nil, "simple.jet", "", "simple.jet")
+ RunJetTestWithSet(t, set, nil, nil, "extension", "", "extension.jet.html")
+ RunJetTestWithSet(t, set, nil, nil, "extension.jet.html", "", "extension.jet.html")
+ RunJetTestWithSet(t, set, nil, nil, "./sub/subextend", "", "simple - simple.jet - extension.jet.html")
+ RunJetTestWithSet(t, set, nil, nil, "./sub/extend", "", "simple - simple.jet - extension.jet.html")
+ for key, _ := range set.templates {
+ t.Log(key)
}
-
- data.Set("complex", &ComplexType{})
-
- evalTestCase(t, data, nil, "BugInGetValueWithPtrMethod",
- `{{complex.User.GetName()}}`, ``)
}
-type devNull struct{}
-
-func (*devNull) Write(_ []byte) (int, error) {
- return 0, nil
-}
-
-var stdSet = template.New("base")
-
-func dummy(a string) string {
- return a
-}
-func init() {
- stdSet.Funcs(template.FuncMap{"dummy": dummy})
- _, err := stdSet.Parse(`
- {{define "actionNode_dummy"}}hello {{dummy "WORLD"}}{{end}}
- {{define "noAllocFn"}}hello {{ "José" }} {{1}} {{ "José" }} {{end}}
- {{define "rangeOverUsers_Set"}}{{range $index,$val := . }}{{$index}}:{{$val.Name}}-{{$val.Email}}{{end}}{{end}}
- {{define "rangeOverUsers"}}{{range . }}{{.Name}}-{{.Email}}{{end}}{{end}}
- `)
- if err != nil {
- println(err.Error())
+func TestSet_Parse(t *testing.T) {
+ set := NewHTMLSet("./testData/resolve")
+
+ var c int64 = 100
+
+ group := &sync.WaitGroup{}
+ for i, l := int64(0), c; i < l; i++ {
+ (func() {
+ template, _ := set.Parse("TestTemplate", `{{extends "sub/extend"}}`)
+ RunJetTestWithTemplate(t, template, nil, nil, "simple - simple.jet - extension.jet.html")
+ if len(set.templates) > 0 {
+ t.Fail()
+ }
+ group.Add(1)
+ runtime.SetFinalizer(template, func(ob interface{}) {
+ group.Done()
+ })
+ })()
}
- evalTemplateSet.AddGlobal("dummy", dummy)
- evalTemplateSet.LoadTemplate("actionNode_dummy", `hello {{dummy("WORLD")}}`)
- evalTemplateSet.LoadTemplate("noAllocFn", `hello {{ "José" }} {{1}} {{ "José" }}`)
- evalTemplateSet.LoadTemplate("rangeOverUsers", `{{range .}}{{.Name}}-{{.Email}}{{end}}`)
- evalTemplateSet.LoadTemplate("rangeOverUsers_Set", `{{range index,user:= . }}{{index}}{{user.Name}}-{{user.Email}}{{end}}`)
-}
-
-var ww io.Writer = (*devNull)(nil)
-var users = []*User{
- &User{"Mario Santos", "mario@gmail.com"},
- &User{"Joel Silva", "joelsilva@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Mario Santos", "mario@gmail.com"},
- &User{"Joel Silva", "joelsilva@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Mario Santos", "mario@gmail.com"},
- &User{"Joel Silva", "joelsilva@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Mario Santos", "mario@gmail.com"},
- &User{"Joel Silva", "joelsilva@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Mario Santos", "mario@gmail.com"},
- &User{"Joel Silva", "joelsilva@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Mario Santos", "mario@gmail.com"},
- &User{"Joel Silva", "joelsilva@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
- &User{"Luis Santana", "luis.santana@gmail.com"},
+ runtime.GC()
+ group.Wait()
}
func BenchmarkSimpleAction(b *testing.B) {
- t, _ := evalTemplateSet.getTemplate("actionNode_dummy")
+ t, _ := JetTestingSet.GetTemplate("actionNode_dummy")
for i := 0; i < b.N; i++ {
err := t.Execute(ww, nil, nil)
if err != nil {
@@ -309,14 +368,14 @@ func BenchmarkSimpleAction(b *testing.B) {
}
func BenchmarkSimpleActionNoAlloc(b *testing.B) {
- t, _ := evalTemplateSet.getTemplate("noAllocFn")
+ t, _ := JetTestingSet.GetTemplate("noAllocFn")
for i := 0; i < b.N; i++ {
t.Execute(ww, nil, nil)
}
}
func BenchmarkRangeSimple(b *testing.B) {
- t, _ := evalTemplateSet.getTemplate("rangeOverUsers")
+ t, _ := JetTestingSet.GetTemplate("rangeOverUsers")
for i := 0; i < b.N; i++ {
err := t.Execute(ww, nil, &users)
if err != nil {
@@ -326,7 +385,7 @@ func BenchmarkRangeSimple(b *testing.B) {
}
func BenchmarkRangeSimpleSet(b *testing.B) {
- t, _ := evalTemplateSet.getTemplate("rangeOverUsers_Set")
+ t, _ := JetTestingSet.GetTemplate("rangeOverUsers_Set")
for i := 0; i < b.N; i++ {
err := t.Execute(ww, nil, &users)
if err != nil {
@@ -374,3 +433,42 @@ func BenchmarkRangeSimpleSetStd(b *testing.B) {
}
}
}
+
+func BenchmarkNewBlockYield(b *testing.B) {
+ t, _ := JetTestingSet.GetTemplate("BenchNewBlock")
+ b.SetParallelism(10000)
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ err := t.Execute(ww, nil, nil)
+ if err != nil {
+ b.Error(err.Error())
+ }
+ }
+ })
+
+}
+
+func BenchmarkDynamicFunc(b *testing.B) {
+
+ var variables = VarMap{}.Set("dummy", dummy)
+ t, _ := JetTestingSet.GetTemplate("actionNode_dummy")
+ for i := 0; i < b.N; i++ {
+ err := t.Execute(ww, variables, nil)
+ if err != nil {
+ b.Error(err.Error())
+ }
+ }
+}
+
+func BenchmarkJetFunc(b *testing.B) {
+ var variables = VarMap{}.SetFunc("dummy", func(a Arguments) reflect.Value {
+ return reflect.ValueOf(dummy(a.Get(0).String()))
+ })
+ t, _ := JetTestingSet.GetTemplate("actionNode_dummy")
+ for i := 0; i < b.N; i++ {
+ err := t.Execute(ww, variables, nil)
+ if err != nil {
+ b.Error(err.Error())
+ }
+ }
+}
diff --git a/example/devop.yml b/example/devop.yml
index 8203d20..6a836cc 100644
--- a/example/devop.yml
+++ b/example/devop.yml
@@ -6,7 +6,6 @@ gobuild:
stderr: true
stdout: true
gorun:
- match: "\\.jet$"
command: "./example"
env:
- PORT=:8890
diff --git a/example/main.go b/example/main.go
index 52c7e40..1f6f3c4 100644
--- a/example/main.go
+++ b/example/main.go
@@ -2,10 +2,16 @@
package main
import (
- "github.com/CloudyKit/jet"
+ "bytes"
+ "encoding/base64"
+ "fmt"
"log"
"net/http"
"os"
+ "reflect"
+ "strings"
+
+ "github.com/CloudyKit/jet"
)
var views = jet.NewHTMLSet("./views")
@@ -15,24 +21,97 @@ type tTODO struct {
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
+}
+
+// Render implements jet.Renderer interface
+func (t *tTODO) Render(r *jet.Runtime) {
+ done := "yes"
+ if !t.Done {
+ done = "no"
+ }
+ r.Write([]byte(fmt.Sprintf("TODO: %s (done: %s)", t.Text, done)))
+}
+
func main() {
- //todo: remove in production
+ // remove in production
views.SetDevelopmentMode(true)
+ views.AddGlobalFunc("base64", func(a jet.Arguments) 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()))
+ })
var todos = map[string]*tTODO{
- "add an show todo page": &tTODO{Text: "Add an show todo page to the example project", Done: true},
- "add an add todo page": &tTODO{Text: "Add an add todo page to the example project"},
- "add an update todo page": &tTODO{Text: "Add an update todo page to the example project"},
- "add an delete todo page": &tTODO{Text: "Add an delete todo page to the example project"},
+ "example-todo-1": &tTODO{Text: "Add an show todo page to the example project", Done: true},
+ "example-todo-2": &tTODO{Text: "Add an add todo page to the example project"},
+ "example-todo-3": &tTODO{Text: "Add an update todo page to the example project"},
+ "example-todo-4": &tTODO{Text: "Add an delete todo page to the example project", Done: true},
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- view, err := views.GetTemplate("showtodos.jet")
+ view, err := views.GetTemplate("todos/index.jet")
if err != nil {
log.Println("Unexpected template err:", err.Error())
}
view.Execute(w, nil, todos)
})
+ http.HandleFunc("/todo", func(w http.ResponseWriter, r *http.Request) {
+ view, err := views.GetTemplate("todos/show.jet")
+ if err != nil {
+ log.Println("Unexpected template err:", err.Error())
+ }
+ id := r.URL.Query().Get("id")
+ todo, ok := todos[id]
+ if !ok {
+ http.Redirect(w, r, "/", http.StatusNotFound)
+ }
+ view.Execute(w, nil, todo)
+ })
+ http.HandleFunc("/all-done", func(w http.ResponseWriter, r *http.Request) {
+ view, err := views.GetTemplate("todos/index.jet")
+ if err != nil {
+ log.Println("Unexpected template err:", err.Error())
+ }
+ view.Execute(w, jet.VarMap{"showingAllDone": reflect.ValueOf(true)}, (&doneTODOs{}).New(todos))
+ })
+
+ port := os.Getenv("PORT")
+ if len(port) == 0 {
+ port = ":8080"
+ } else if !strings.HasPrefix(":", port) {
+ port = ":" + port
+ }
- http.ListenAndServe(os.Getenv("PORT"), nil)
+ log.Println("Serving on " + port)
+ http.ListenAndServe(port, nil)
}
diff --git a/example/views/layout.jet b/example/views/layout.jet
deleted file mode 100644
index 2aa080b..0000000
--- a/example/views/layout.jet
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-{{ isset(title) ? title : "" }}
-
-
-{{ block documentBody }}{{ end }}
-
-
\ No newline at end of file
diff --git a/example/views/layouts/application.jet b/example/views/layouts/application.jet
new file mode 100644
index 0000000..9fce236
--- /dev/null
+++ b/example/views/layouts/application.jet
@@ -0,0 +1,10 @@
+
+
+
+
+ {{ isset(title) ? title : "" }}
+
+
+ {{block documentBody()}}{{end}}
+
+
diff --git a/example/views/showtodos.jet b/example/views/showtodos.jet
deleted file mode 100644
index fa7f664..0000000
--- a/example/views/showtodos.jet
+++ /dev/null
@@ -1,12 +0,0 @@
-{{ extends "layout.jet" }}
-
-{{ block documentBody }}
- List of todos
-
- {{ range id,value := . }}
-
- {{value.Text}} UP | DL
-
- {{ end }}
-
-{{ end }}
\ No newline at end of file
diff --git a/example/views/todos/index.jet b/example/views/todos/index.jet
new file mode 100644
index 0000000..3f1567c
--- /dev/null
+++ b/example/views/todos/index.jet
@@ -0,0 +1,29 @@
+{{extends "layouts/application.jet"}}
+
+{{block button(label, href="javascript:void(0)")}}
+ {{ label }}
+{{end}}
+
+{{block ul()}}
+
+{{end}}
+
+{{block documentBody()}}
+ List of TODOs
+ {{if isset(showingAllDone) && showingAllDone}}
+ Showing only TODOs that are done
+ {{else}}
+ Show only TODOs that are done
+ {{end}}
+
+ {{yield ul() content}}
+ {{range id, value := .}}
+
+ {{ value.Text }}
+ {{yield button(label="UP", href="/update/?id="+base64(id))}} - {{yield button(href="/delete/?id="+id, label="DL")}}
+
+ {{end}}
+ {{end}}
+{{end}}
diff --git a/example/views/todos/show.jet b/example/views/todos/show.jet
new file mode 100644
index 0000000..677b7cb
--- /dev/null
+++ b/example/views/todos/show.jet
@@ -0,0 +1,9 @@
+{{extends "layouts/application.jet"}}
+
+{{block documentBody()}}
+ Show TODO
+ This uses a custom renderer by implementing the jet.Renderer interface.
+
+ {{ . }}
+
+{{end}}
diff --git a/func.go b/func.go
new file mode 100644
index 0000000..47b4e8e
--- /dev/null
+++ b/func.go
@@ -0,0 +1,58 @@
+// Copyright 2016 José Santos
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package jet
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Arguments holds the arguments passed to jet.Func.
+type Arguments struct {
+ runtime *Runtime
+ argExpr []Expression
+ argVal []reflect.Value
+}
+
+// Get gets an argument by index.
+func (a *Arguments) Get(argumentIndex int) reflect.Value {
+ if argumentIndex < len(a.argVal) {
+ return a.argVal[argumentIndex]
+ }
+ if argumentIndex < len(a.argVal)+len(a.argExpr) {
+ return a.runtime.evalPrimaryExpressionGroup(a.argExpr[argumentIndex-len(a.argVal)])
+ }
+ return reflect.Value{}
+}
+
+// Panicf panics with formatted error message.
+func (a *Arguments) Panicf(format string, v ...interface{}) {
+ panic(fmt.Errorf(format, v...))
+}
+
+// RequireNumOfArguments panics if the number of arguments is not in the range specified by min and max.
+// In case there is no minimum pass -1, in case there is no maximum pass -1 respectively.
+func (a *Arguments) RequireNumOfArguments(funcname string, min, max int) {
+ num := len(a.argExpr) + len(a.argVal)
+ if min >= 0 && num < min {
+ a.Panicf("unexpected number of arguments in a call to %s", funcname)
+ } else if max >= 0 && num > max {
+ a.Panicf("unexpected number of arguments in a call to %s", funcname)
+ }
+}
+
+// Func function implementing this type is called directly, which is faster than calling through reflect.
+// If a function is being called many times in the execution of a template, you may consider implementing
+// a wrapper for that function implementing a Func.
+type Func func(Arguments) reflect.Value
diff --git a/lex.go b/lex.go
index 48c079a..ee61d64 100644
--- a/lex.go
+++ b/lex.go
@@ -86,20 +86,20 @@ const (
itemKeyword // used only to delimit the keywords
itemExtends
itemBlock
+ itemYield
+ itemContent
itemInclude
itemElse
itemEnd
itemIf
itemNil
itemRange
- itemYield
itemImport
itemAnd
itemOr
itemNot
- itemSet
- itemIsset
- itemLen
+ itemMSG
+ itemTrans
)
var key = map[string]itemType{
@@ -110,17 +110,19 @@ var key = map[string]itemType{
"block": itemBlock,
"yield": itemYield,
- "else": itemElse,
- "end": itemEnd,
- "if": itemIf,
- "set": itemSet,
+ "else": itemElse,
+ "end": itemEnd,
+ "if": itemIf,
+
"range": itemRange,
"nil": itemNil,
"and": itemAnd,
"or": itemOr,
"not": itemNot,
- "isset": itemIsset,
- "len": itemLen,
+
+ "content": itemContent,
+ "msg": itemMSG,
+ "trans": itemTrans,
}
const eof = -1
@@ -241,8 +243,6 @@ func (l *lexer) run() {
close(l.items)
}
-// state functions
-
const (
leftDelim = "{{"
rightDelim = "}}"
@@ -250,22 +250,27 @@ const (
rightComment = "*}"
)
+// state functions
func lexText(l *lexer) stateFn {
for {
- if strings.HasPrefix(l.input[l.pos:], leftDelim) {
- if l.pos > l.start {
- l.emit(itemText)
+ if i := strings.IndexByte(l.input[l.pos:], '{'); i == -1 {
+ l.pos = Pos(len(l.input))
+ break
+ } else {
+ l.pos += Pos(i)
+ if strings.HasPrefix(l.input[l.pos:], leftDelim) {
+ if l.pos > l.start {
+ l.emit(itemText)
+ }
+ return lexLeftDelim
}
- return lexLeftDelim
- }
-
- if strings.HasPrefix(l.input[l.pos:], leftComment) {
- if l.pos > l.start {
- l.emit(itemText)
+ if strings.HasPrefix(l.input[l.pos:], leftComment) {
+ if l.pos > l.start {
+ l.emit(itemText)
+ }
+ return lexComment
}
- return lexComment
}
-
if l.next() == eof {
break
}
diff --git a/node.go b/node.go
index 5f7f8b0..6ae3362 100644
--- a/node.go
+++ b/node.go
@@ -1,16 +1,16 @@
-//Copyright 2016 José Santos
+// Copyright 2016 José Santos
//
-//Licensed under the Apache License, Version 2.0 (the "License");
-//you may not use this file except in compliance with the License.
-//You may obtain a copy of the License at
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-//http://www.apache.org/licenses/LICENSE-2.0
+// http://www.apache.org/licenses/LICENSE-2.0
//
-//Unless required by applicable law or agreed to in writing, software
-//distributed under the License is distributed on an "AS IS" BASIS,
-//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-//See the License for the specific language governing permissions and
-//limitations under the License.
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
package jet
@@ -34,15 +34,15 @@ type Expression interface {
Node
}
-//Pos represents a byte position in the original input text from which
-//this template was parsed.
+// Pos represents a byte position in the original input text from which
+// this template was parsed.
type Pos int
func (p Pos) Position() Pos {
return p
}
-//NodeType identifies the type of a parse tree node.
+// NodeType identifies the type of a parse tree node.
type NodeType int
type NodeBase struct {
@@ -64,8 +64,8 @@ func (node *NodeBase) errorf(format string, v ...interface{}) {
panic(fmt.Errorf("Jet Runtime Error(%q:%d): %s", node.TemplateName, node.Line, fmt.Sprintf(format, v...)))
}
-//Type returns itself and provides an easy default implementation
-//for embedding in a Node. Embedded in all non-trivial Nodes.
+// Type returns itself and provides an easy default implementation
+// for embedding in a Node. Embedded in all non-trivial Nodes.
func (t NodeType) Type() NodeType {
return t
}
@@ -83,6 +83,7 @@ const (
NodeList //A list of Nodes.
NodePipe //A pipeline of commands.
NodeRange //A range action.
+ nodeContent
//NodeWith //A with action.
NodeBlock
NodeInclude
@@ -100,17 +101,15 @@ const (
NodeLogicalExpr
NodeCallExpr
NodeNotExpr
- NodeIssetExpr
- NodeLenExpr
NodeTernaryExpr
NodeIndexExpr
NodeSliceExpr
endExpressions
)
-//Nodes.
+// Nodes.
-//ListNode holds a sequence of nodes.
+// ListNode holds a sequence of nodes.
type ListNode struct {
NodeBase
Nodes []Node //The element nodes in lexical order.
@@ -128,7 +127,7 @@ func (l *ListNode) String() string {
return b.String()
}
-//TextNode holds plain text.
+// TextNode holds plain text.
type TextNode struct {
NodeBase
Text []byte
@@ -138,7 +137,7 @@ func (t *TextNode) String() string {
return fmt.Sprintf(textFormat, t.Text)
}
-//PipeNode holds a pipeline with optional declaration
+// PipeNode holds a pipeline with optional declaration
type PipeNode struct {
NodeBase //The line number in the input. Deprecated: Kept for compatibility.
Cmds []*CommandNode //The commands in lexical order.
@@ -159,9 +158,9 @@ func (p *PipeNode) String() string {
return s
}
-//ActionNode holds an action (something bounded by delimiters).
-//Control actions have their own nodes; ActionNode represents simple
-//ones such as field evaluations and parenthesized pipelines.
+// ActionNode holds an action (something bounded by delimiters).
+// Control actions have their own nodes; ActionNode represents simple
+// ones such as field evaluations and parenthesized pipelines.
type ActionNode struct {
NodeBase
Set *SetNode
@@ -170,12 +169,15 @@ type ActionNode struct {
func (a *ActionNode) String() string {
if a.Set != nil {
+ if a.Pipe == nil {
+ return fmt.Sprintf("{{%s}}", a.Set)
+ }
return fmt.Sprintf("{{%s;%s}}", a.Set, a.Pipe)
}
return fmt.Sprintf("{{%s}}", a.Pipe)
}
-//CommandNode holds a command (a pipeline inside an evaluating action).
+// CommandNode holds a command (a pipeline inside an evaluating action).
type CommandNode struct {
NodeBase
Call bool
@@ -206,7 +208,7 @@ func (c *CommandNode) String() string {
return s
}
-//IdentifierNode holds an identifier.
+// IdentifierNode holds an identifier.
type IdentifierNode struct {
NodeBase
Ident string //The identifier's name.
@@ -216,7 +218,7 @@ func (i *IdentifierNode) String() string {
return i.Ident
}
-//NilNode holds the special identifier 'nil' representing an untyped nil constant.
+// NilNode holds the special identifier 'nil' representing an untyped nil constant.
type NilNode struct {
NodeBase
}
@@ -225,9 +227,9 @@ func (n *NilNode) String() string {
return "nil"
}
-//FieldNode holds a field (identifier starting with '.').
-//The names may be chained ('.x.y').
-//The period is dropped from each ident.
+// FieldNode holds a field (identifier starting with '.').
+// The names may be chained ('.x.y').
+// The period is dropped from each ident.
type FieldNode struct {
NodeBase
Ident []string //The identifiers in lexical order.
@@ -241,16 +243,16 @@ func (f *FieldNode) String() string {
return s
}
-//ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
-//The names may be chained ('.x.y').
-//The periods are dropped from each ident.
+// ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
+// The names may be chained ('.x.y').
+// The periods are dropped from each ident.
type ChainNode struct {
NodeBase
Node Node
Field []string //The identifiers in lexical order.
}
-//Add adds the named field (which should start with a period) to the end of the chain.
+// Add adds the named field (which should start with a period) to the end of the chain.
func (c *ChainNode) Add(field string) {
if len(field) == 0 || field[0] != '.' {
panic("no dot in field")
@@ -273,7 +275,7 @@ func (c *ChainNode) String() string {
return s
}
-//BoolNode holds a boolean constant.
+// BoolNode holds a boolean constant.
type BoolNode struct {
NodeBase
True bool //The value of the boolean constant.
@@ -286,9 +288,9 @@ func (b *BoolNode) String() string {
return "false"
}
-//NumberNode holds a number: signed or unsigned integer, float, or complex.
-//The value is parsed and stored under all the types that can represent the value.
-//This simulates in a small amount of code the behavior of Go's ideal constants.
+// NumberNode holds a number: signed or unsigned integer, float, or complex.
+// The value is parsed and stored under all the types that can represent the value.
+// This simulates in a small amount of code the behavior of Go's ideal constants.
type NumberNode struct {
NodeBase
@@ -303,8 +305,8 @@ type NumberNode struct {
Text string //The original textual representation from the input.
}
-//simplifyComplex pulls out any other types that are represented by the complex number.
-//These all require that the imaginary part be zero.
+// simplifyComplex pulls out any other types that are represented by the complex number.
+// These all require that the imaginary part be zero.
func (n *NumberNode) simplifyComplex() {
n.IsFloat = imag(n.Complex128) == 0
if n.IsFloat {
@@ -324,7 +326,7 @@ func (n *NumberNode) String() string {
return n.Text
}
-//StringNode holds a string constant. The value has been "unquoted".
+// StringNode holds a string constant. The value has been "unquoted".
type StringNode struct {
NodeBase
@@ -336,8 +338,8 @@ func (s *StringNode) String() string {
return s.Quoted
}
-//endNode represents an {{end}} action.
-//It does not appear in the final parse tree.
+// endNode represents an {{end}} action.
+// It does not appear in the final parse tree.
type endNode struct {
NodeBase
}
@@ -346,7 +348,17 @@ func (e *endNode) String() string {
return "{{end}}"
}
-//elseNode represents an {{else}} action. Does not appear in the final tree.
+// endNode represents an {{end}} action.
+// It does not appear in the final parse tree.
+type contentNode struct {
+ NodeBase
+}
+
+func (e *contentNode) String() string {
+ return "{{content}}"
+}
+
+// elseNode represents an {{else}} action. Does not appear in the final tree.
type elseNode struct {
NodeBase //The line number in the input. Deprecated: Kept for compatibility.
}
@@ -355,12 +367,13 @@ func (e *elseNode) String() string {
return "{{else}}"
}
-//SetNode represents a set action, ident( ',' ident)* '=' expression ( ',' expression )*
+// SetNode represents a set action, ident( ',' ident)* '=' expression ( ',' expression )*
type SetNode struct {
NodeBase
- Let bool
- Left []Expression
- Right []Expression
+ Let bool
+ IndexExprGetLookup bool
+ Left []Expression
+ Right []Expression
}
func (set *SetNode) String() string {
@@ -389,7 +402,7 @@ func (set *SetNode) String() string {
return s
}
-//BranchNode is the common representation of if, range, and with.
+// BranchNode is the common representation of if, range, and with.
type BranchNode struct {
NodeBase
Set *SetNode
@@ -424,46 +437,112 @@ func (b *BranchNode) String() string {
}
}
-//IfNode represents an {{if}} action and its commands.
+// IfNode represents an {{if}} action and its commands.
type IfNode struct {
BranchNode
}
-//RangeNode represents a {{range}} action and its commands.
+// RangeNode represents a {{range}} action and its commands.
type RangeNode struct {
BranchNode
}
-//BlockNode represents a {{block }} action.
+type BlockParameter struct {
+ Identifier string
+ Expression Expression
+}
+
+type BlockParameterList struct {
+ NodeBase
+ List []BlockParameter
+}
+
+func (bplist *BlockParameterList) Param(name string) (Expression, int) {
+ for i := 0; i < len(bplist.List); i++ {
+ param := &bplist.List[i]
+ if param.Identifier == name {
+ return param.Expression, i
+ }
+ }
+ return nil, -1
+}
+
+func (bplist *BlockParameterList) String() (str string) {
+ buff := bytes.NewBuffer(nil)
+ for _, bp := range bplist.List {
+ if bp.Identifier == "" {
+ fmt.Fprintf(buff, "%s,", bp.Expression)
+ } else {
+ if bp.Expression == nil {
+ fmt.Fprintf(buff, "%s,", bp.Identifier)
+ } else {
+ fmt.Fprintf(buff, "%s=%s,", bp.Identifier, bp.Expression)
+ }
+ }
+ }
+ if buff.Len() > 0 {
+ str = buff.String()[0 : buff.Len()-1]
+ }
+ return
+}
+
+// BlockNode represents a {{block }} action.
type BlockNode struct {
- NodeBase //The line number in the input. Deprecated: Kept for compatibility.
- Name string //The name of the template (unquoted).
+ NodeBase //The line number in the input. Deprecated: Kept for compatibility.
+ Name string //The name of the template (unquoted).
+
+ Parameters *BlockParameterList
Expression Expression //The command to evaluate as dot for the template.
- List *ListNode
+
+ List *ListNode
+ Content *ListNode
}
func (t *BlockNode) String() string {
+ if t.Content != nil {
+ if t.Expression == nil {
+ return fmt.Sprintf("{{block %s(%s)}}%s{{content}}%s{{end}}", t.Name, t.Parameters, t.List, t.Content)
+ }
+ return fmt.Sprintf("{{block %s(%s) %s}}%s{{content}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.List, t.Content)
+ }
if t.Expression == nil {
- return fmt.Sprintf("{{block %s}}%s{{end}}", t.Name, t.List)
+ return fmt.Sprintf("{{block %s(%s)}}%s{{end}}", t.Name, t.Parameters, t.List)
}
- return fmt.Sprintf("{{block %s %s}}%s{{end}}", t.Name, t.Expression, t.List)
+ return fmt.Sprintf("{{block %s(%s) %s}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.List)
}
-//YieldNode represents a {{yield}} action
+// YieldNode represents a {{yield}} action
type YieldNode struct {
- NodeBase //The line number in the input. Deprecated: Kept for compatibility.
- Name string //The name of the template (unquoted).
+ NodeBase //The line number in the input. Deprecated: Kept for compatibility.
+ Name string //The name of the template (unquoted).
+ Parameters *BlockParameterList
Expression Expression //The command to evaluate as dot for the template.
+ Content *ListNode
+ IsContent bool
}
func (t *YieldNode) String() string {
+ if t.IsContent {
+ if t.Expression == nil {
+ return "{{yield content}}"
+ }
+ return fmt.Sprintf("{{yield content %s}}", t.Expression)
+ }
+
+ if t.Content != nil {
+ if t.Expression == nil {
+ return fmt.Sprintf("{{yield %s(%s) content}}%s{{end}}", t.Name, t.Parameters, t.Content)
+ }
+ return fmt.Sprintf("{{yield %s(%s) %s content}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.Content)
+ }
+
if t.Expression == nil {
- return fmt.Sprintf("{{yield %s}}", t.Name)
+ return fmt.Sprintf("{{yield %s(%s)}}", t.Name, t.Parameters)
}
- return fmt.Sprintf("{{yield %s %s}}", t.Name, t.Expression)
+ return fmt.Sprintf("{{yield %s(%s) %s}}", t.Name, t.Parameters, t.Expression)
}
-//IncludeNode represents a {{include }} action.
+// IncludeNode represents a {{include }} action.
type IncludeNode struct {
NodeBase
Name Expression
@@ -487,38 +566,38 @@ func (node *binaryExprNode) String() string {
return fmt.Sprintf("%s %s %s", node.Left, node.Operator.val, node.Right)
}
-//AdditiveExprNode represents an add or subtract expression
-//ex: expression ( '+' | '-' ) expression
+// AdditiveExprNode represents an add or subtract expression
+// ex: expression ( '+' | '-' ) expression
type AdditiveExprNode struct {
binaryExprNode
}
-//MultiplicativeExprNode represents a multiplication, division, or module expression
-//ex: expression ( '*' | '/' | '%' ) expression
+// MultiplicativeExprNode represents a multiplication, division, or module expression
+// ex: expression ( '*' | '/' | '%' ) expression
type MultiplicativeExprNode struct {
binaryExprNode
}
-//LogicalExprNode represents a boolean expression, 'and' or 'or'
-//ex: expression ( '&&' | '||' ) expression
+// LogicalExprNode represents a boolean expression, 'and' or 'or'
+// ex: expression ( '&&' | '||' ) expression
type LogicalExprNode struct {
binaryExprNode
}
-//ComparativeExprNode represents a comparative expression
-//ex: expression ( '==' | '!=' ) expression
+// ComparativeExprNode represents a comparative expression
+// ex: expression ( '==' | '!=' ) expression
type ComparativeExprNode struct {
binaryExprNode
}
-//NumericComparativeExprNode represents a numeric comparative expression
-//ex: expression ( '<' | '>' | '<=' | '>=' ) expression
+// NumericComparativeExprNode represents a numeric comparative expression
+// ex: expression ( '<' | '>' | '<=' | '>=' ) expression
type NumericComparativeExprNode struct {
binaryExprNode
}
-//NotExprNode represents a negate expression
-//ex: '!' expression
+// NotExprNode represents a negate expression
+// ex: '!' expression
type NotExprNode struct {
NodeBase
Expr Expression
@@ -528,8 +607,8 @@ func (s *NotExprNode) String() string {
return fmt.Sprintf("!%s", s.Expr)
}
-//CallExprNode represents a call expression
-//ex: expression '(' (expression (',' expression)* )? ')'
+// CallExprNode represents a call expression
+// ex: expression '(' (expression (',' expression)* )? ')'
type CallExprNode struct {
NodeBase
BaseExpr Expression
@@ -547,26 +626,8 @@ func (s *CallExprNode) String() string {
return fmt.Sprintf("%s(%s)", s.BaseExpr, arguments)
}
-//ex: builtinToken '(' (expression (',' expression)* )? ')'
-type BuiltinExprNode struct {
- NodeBase
- Name string
- Args []Expression
-}
-
-func (s *BuiltinExprNode) String() string {
- arguments := ""
- for i, expr := range s.Args {
- if i > 0 {
- arguments += ", "
- }
- arguments += expr.String()
- }
- return fmt.Sprintf("%s(%s)", s.Name, arguments)
-}
-
-//TernaryExprNod represents a ternary expression,
-//ex: expression '?' expression ':' expression
+// TernaryExprNod represents a ternary expression,
+// ex: expression '?' expression ':' expression
type TernaryExprNode struct {
NodeBase
Boolean, Left, Right Expression
diff --git a/parse.go b/parse.go
index d1d2893..2a73857 100644
--- a/parse.go
+++ b/parse.go
@@ -25,7 +25,7 @@ func unquote(text string) (string, error) {
return strconv.Unquote(text)
}
-// parser is the representation of a single parsed template.
+// Template is the representation of a single parsed template.
type Template struct {
Name string // name of the template represented by the tree.
ParseName string // name of the top-level template during parsing, for error messages.
@@ -177,7 +177,7 @@ func (s *Set) parse(name, text string) (t *Template, err error) {
t.addBlocks(_import.processedBlocks)
}
t.addBlocks(t.passedBlocks)
- return t, nil
+ return t, err
}
func (t *Template) expectString(context string) string {
@@ -210,12 +210,12 @@ func (t *Template) parseTemplate() (next Node) {
t.errorf("Unexpected extends clause, all import clause should come after extends clause")
}
var err error
- t.extends, err = t.set.loadTemplate(s, "")
+ t.extends, err = t.set.getTemplateWhileParsing(t.Name, s)
if err != nil {
t.error(err)
}
} else {
- tt, err := t.set.loadTemplate(s, "")
+ tt, err := t.set.getTemplateWhileParsing(t.Name, s)
if err != nil {
t.error(err)
}
@@ -234,7 +234,7 @@ func (t *Template) parseTemplate() (next Node) {
for t.peek().typ != itemEOF {
switch n := t.textOrAction(); n.Type() {
- case nodeEnd, nodeElse:
+ case nodeEnd, nodeElse, nodeContent:
t.errorf("unexpected %s", n)
default:
t.root.append(n)
@@ -280,46 +280,143 @@ func IsEmptyTree(n Node) bool {
return false
}
-// parseDefinition parses a {{block Ident pipeline?}} ... {{end}} template definition and
-// installs the definition in the treeSet map. The "define" keyword has already
-// been scanned.
+func (t *Template) blockParametersList(isDeclaring bool, context string) *BlockParameterList {
+ block := &BlockParameterList{}
+
+ t.expect(itemLeftParen, context)
+ for {
+ var expression Expression
+ next := t.nextNonSpace()
+ if next.typ == itemIdentifier {
+ identifier := next.val
+ next2 := t.nextNonSpace()
+ switch next2.typ {
+ case itemComma, itemRightParen:
+ block.List = append(block.List, BlockParameter{Identifier: identifier})
+ next = next2
+ case itemAssign:
+ expression, next = t.parseExpression(context)
+ block.List = append(block.List, BlockParameter{Identifier: identifier, Expression: expression})
+ default:
+ if !isDeclaring {
+ switch next2.typ {
+ case itemComma, itemRightParen:
+ default:
+ t.backup2(next)
+ expression, next = t.parseExpression(context)
+ block.List = append(block.List, BlockParameter{Expression: expression})
+ }
+ } else {
+ t.unexpected(next2, context)
+ }
+ }
+ } else if !isDeclaring {
+ switch next.typ {
+ case itemComma, itemRightParen:
+ default:
+ t.backup()
+ expression, next = t.parseExpression(context)
+ block.List = append(block.List, BlockParameter{Expression: expression})
+ }
+ }
+
+ if next.typ != itemComma {
+ t.backup()
+ break
+ }
+ }
+ t.expect(itemRightParen, context)
+ return block
+}
+
func (t *Template) parseBlock() Node {
- const context = "block clause"
- name := t.expect(itemIdentifier, context)
+ const context = "block clause"
var pipe Expression
+ name := t.expect(itemIdentifier, context)
+ bplist := t.blockParametersList(true, context)
+
if t.peekNonSpace().typ != itemRightDelim {
- pipe = t.expression("block")
+ pipe = t.expression(context)
}
+
t.expect(itemRightDelim, context)
+
list, end := t.itemList()
- if end.Type() != nodeEnd {
+ var contentList *ListNode
+
+ if end.Type() == nodeContent {
+ contentList, end = t.itemList()
+ if end.Type() != nodeEnd {
+ t.errorf("unexpected %s in %s", end, context)
+ }
+ } else if end.Type() != nodeEnd {
t.errorf("unexpected %s in %s", end, context)
}
- block := t.newBlock(name.pos, t.lex.lineNumber(), name.val, pipe, list)
+ block := t.newBlock(name.pos, t.lex.lineNumber(), name.val, bplist, pipe, list, contentList)
t.passedBlocks[block.Name] = block
return block
}
func (t *Template) parseYield() Node {
const context = "yield clause"
- var pipe Expression
- name := t.expect(itemIdentifier, context)
-
- if t.peekNonSpace().typ != itemRightDelim {
- pipe = t.expression("yield")
+ var (
+ pipe Expression
+ name item
+ bplist *BlockParameterList
+ content *ListNode
+ end Node
+ )
+
+ // content yield {{yield content}}
+ name = t.nextNonSpace()
+ if name.typ == itemContent {
+ if t.peekNonSpace().typ != itemRightDelim {
+ pipe = t.expression(context)
+ }
+ t.expect(itemRightDelim, context)
+ return t.newYield(name.pos, t.lex.lineNumber(), "", nil, pipe, nil, true)
+ } else if name.typ != itemIdentifier {
+ t.unexpected(name, context)
+ }
+ bplist = t.blockParametersList(false, context)
+ typ := t.peekNonSpace().typ
+ if typ != itemRightDelim {
+ if typ == itemContent {
+ t.nextNonSpace()
+ t.expect(itemRightDelim, context)
+ content, end = t.itemList()
+ if end.Type() != nodeEnd {
+ t.errorf("unexpected %s in %s", end, context)
+ }
+ } else {
+ pipe = t.expression("yield")
+ if t.peekNonSpace().typ == itemContent {
+ t.nextNonSpace()
+ t.expect(itemRightDelim, context)
+ content, end = t.itemList()
+ if end.Type() != nodeEnd {
+ t.errorf("unexpected %s in %s", end, context)
+ }
+ } else {
+ t.expect(itemRightDelim, context)
+ }
+ }
+ } else {
+ t.expect(itemRightDelim, context)
}
- t.expect(itemRightDelim, context)
- return t.newYield(name.pos, t.lex.lineNumber(), name.val, pipe)
+
+ return t.newYield(name.pos, t.lex.lineNumber(), name.val, bplist, pipe, content, false)
}
func (t *Template) parseInclude() Node {
+ var pipe Expression
name := t.expression("include")
- var pipe Expression
+
if t.nextNonSpace().typ != itemRightDelim {
t.backup()
pipe = t.expression("include")
@@ -330,15 +427,32 @@ func (t *Template) parseInclude() Node {
return t.newInclude(name.Position(), t.lex.lineNumber(), name, pipe)
}
-// itemList:
+// itemListBlock:
// textOrAction*
// Terminates at {{end}} or {{else}}, returned separately.
+func (t *Template) itemListBlock() (list *ListNode, next Node) {
+ list = t.newList(t.peekNonSpace().pos)
+ for t.peekNonSpace().typ != itemEOF {
+ n := t.textOrAction()
+ switch n.Type() {
+ case nodeEnd, nodeContent:
+ return list, n
+ }
+ list.append(n)
+ }
+ t.errorf("unexpected EOF")
+ return
+}
+
+// itemListControl:
+// textOrAction*
+// Terminates at {{end}}, returned separately.
func (t *Template) itemList() (list *ListNode, next Node) {
list = t.newList(t.peekNonSpace().pos)
for t.peekNonSpace().typ != itemEOF {
n := t.textOrAction()
switch n.Type() {
- case nodeEnd, nodeElse:
+ case nodeEnd, nodeElse, nodeContent:
return list, n
}
list.append(n)
@@ -361,17 +475,14 @@ func (t *Template) textOrAction() Node {
return nil
}
-// Action:
-// control
-// command ("|" command)*
-// Left delim is past. Now get actions.
-// First word could be a keyword such as range.
func (t *Template) action() (n Node) {
switch token := t.nextNonSpace(); token.typ {
case itemElse:
return t.elseControl()
case itemEnd:
return t.endControl()
+ case itemContent:
+ return t.contentControl()
case itemIf:
return t.ifControl()
case itemRange:
@@ -382,10 +493,9 @@ func (t *Template) action() (n Node) {
return t.parseInclude()
case itemYield:
return t.parseYield()
-
}
- t.backup()
+ t.backup()
action := t.newAction(t.peek().pos, t.lex.lineNumber())
expr := t.assignmentOrExpression("command")
@@ -407,15 +517,16 @@ func (t *Template) logicalExpression(context string) (Expression, item) {
}
return left, endtoken
}
-func (t *Template) parserExpression(context string) (Expression, item) {
+
+func (t *Template) parseExpression(context string) (Expression, item) {
expression, endtoken := t.logicalExpression(context)
if endtoken.typ == itemTernary {
var left, right Expression
- left, endtoken = t.parserExpression(context)
+ left, endtoken = t.parseExpression(context)
if endtoken.typ != itemColon {
t.unexpected(endtoken, "ternary expression")
}
- right, endtoken = t.parserExpression(context)
+ right, endtoken = t.parseExpression(context)
expression = t.newTernaryExpr(expression.Position(), t.lex.lineNumber(), expression, left, right)
}
return expression, endtoken
@@ -478,7 +589,7 @@ func (t *Template) assignmentOrExpression(context string) (operand Expression) {
var isSet bool
var isLet bool
var returned item
- operand, returned = t.parserExpression(context)
+ operand, returned = t.parseExpression(context)
pos := operand.Position()
if returned.typ == itemComma || returned.typ == itemAssign {
isSet = true
@@ -502,7 +613,7 @@ func (t *Template) assignmentOrExpression(context string) (operand Expression) {
switch returned.typ {
case itemComma:
- operand, returned = t.parserExpression(context)
+ operand, returned = t.parseExpression(context)
case itemAssign:
isLet = returned.val == ":="
break leftloop
@@ -520,7 +631,7 @@ func (t *Template) assignmentOrExpression(context string) (operand Expression) {
}
for {
- operand, returned = t.parserExpression("assignment")
+ operand, returned = t.parseExpression("assignment")
right = append(right, operand)
if returned.typ != itemComma {
t.backup()
@@ -528,16 +639,22 @@ func (t *Template) assignmentOrExpression(context string) (operand Expression) {
}
}
+ var isIndexExprGetLookup bool
+
if context == "range" {
if len(left) > 2 || len(right) > 1 {
t.errorf("unexpected number of operands in assign on range")
}
} else {
if len(left) != len(right) {
- t.errorf("unexpected number of operands in assign on range")
+ if len(left) == 2 && len(right) == 1 && right[0].Type() == NodeIndexExpr {
+ isIndexExprGetLookup = true
+ } else {
+ t.errorf("unexpected number of operands in assign on range")
+ }
}
}
- operand = t.newSet(pos, line, isLet, left, right)
+ operand = t.newSet(pos, line, isLet, isIndexExprGetLookup, left, right)
return
}
@@ -545,7 +662,7 @@ func (t *Template) assignmentOrExpression(context string) (operand Expression) {
}
func (t *Template) expression(context string) Expression {
- expr, tk := t.parserExpression(context)
+ expr, tk := t.parseExpression(context)
if expr == nil {
t.unexpected(tk, context)
}
@@ -553,8 +670,6 @@ func (t *Template) expression(context string) Expression {
return expr
}
-// Pipeline:
-// declarations? command ('|' command)*
func (t *Template) pipeline(context string, baseExprMutate Expression) (pipe *PipeNode) {
pos := t.peekNonSpace().pos
pipe = t.newPipeline(pos, t.lex.lineNumber())
@@ -578,7 +693,7 @@ loop:
for {
switch token.typ {
case itemBool, itemCharConstant, itemComplex, itemField, itemIdentifier,
- itemNumber, itemNil, itemRawString, itemString, itemLeftParen, itemNot, itemIsset, itemLen:
+ itemNumber, itemNil, itemRawString, itemString, itemLeftParen, itemNot:
t.backup()
pipe.append(t.command(nil))
token = t.nextNonSpace()
@@ -599,10 +714,6 @@ loop:
return
}
-// command:
-// operand (:(space operand)*)?
-// space-separated arguments up to a pipeline character or right delimiter.
-// we consume the pipe character but leave the right delim to terminate the action.
func (t *Template) command(baseExpr Expression) *CommandNode {
cmd := t.newCommand(t.peekNonSpace().pos)
@@ -671,7 +782,7 @@ RESET:
//found colon is slice expression
if t.peekNonSpace().typ != itemColon {
- index, next = t.parserExpression("index|slice expression")
+ index, next = t.parseExpression("index|slice expression")
} else {
next = t.nextNonSpace()
}
@@ -703,7 +814,7 @@ func (t *Template) parseArguments() (args []Expression) {
if t.peekNonSpace().typ != itemRightParen {
loop:
for {
- expr, endtoken := t.parserExpression("call expression")
+ expr, endtoken := t.parseExpression("call expression")
args = append(args, expr)
switch endtoken.typ {
case itemComma:
@@ -786,16 +897,16 @@ func (t *Template) parseControl(allowElseIf bool, context string) (pos Pos, line
}
// If:
-// {{if pipeline}} itemList {{end}}
-// {{if pipeline}} itemList {{else}} itemList {{end}}
+// {{if expression}} itemList {{end}}
+// {{if expression}} itemList {{else}} itemList {{end}}
// If keyword is past.
func (t *Template) ifControl() Node {
return t.newIf(t.parseControl(true, "if"))
}
// Range:
-// {{range pipeline}} itemList {{end}}
-// {{range pipeline}} itemList {{else}} itemList {{end}}
+// {{range expression}} itemList {{end}}
+// {{range expression}} itemList {{else}} itemList {{end}}
// Range keyword is past.
func (t *Template) rangeControl() Node {
return t.newRange(t.parseControl(false, "range"))
@@ -808,6 +919,13 @@ func (t *Template) endControl() Node {
return t.newEnd(t.expect(itemRightDelim, "end").pos)
}
+// Content:
+// {{content}}
+// Content keyword is past.
+func (t *Template) contentControl() Node {
+ return t.newContent(t.expect(itemRightDelim, "content").pos)
+}
+
// Else:
// {{else}}
// Else keyword is past.
@@ -826,26 +944,14 @@ func (t *Template) elseControl() Node {
// function (identifier)
// .
// .Field
-// $
-// '(' pipeline ')'
+// variable
+// '(' expression ')'
// A term is a simple "expression".
// A nil return means the next item is not a term.
func (t *Template) term() Node {
switch token := t.nextNonSpace(); token.typ {
case itemError:
t.errorf("%s", token.val)
- case itemLen:
- node := t.newBuiltinExpr(token.pos, t.lex.lineNumber(), token.val, NodeLenExpr)
- t.expect(itemLeftParen, "builtin len call")
- node.Args = []Expression{t.expression("builtin len call")}
- t.expect(itemRightParen, "builtin len call")
- return node
- case itemIsset:
- node := t.newBuiltinExpr(token.pos, t.lex.lineNumber(), token.val, NodeIssetExpr)
- t.expect(itemLeftParen, "builtin isset call")
- node.Args = t.parseArguments()
- t.expect(itemRightParen, "builtin isset call")
- return node
case itemIdentifier:
return t.newIdentifier(token.val, token.pos, t.lex.lineNumber())
case itemNil:
diff --git a/parse_test.go b/parse_test.go
index 73be7ae..fc2992a 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -21,7 +21,7 @@ import (
"testing"
)
-var parseSet = NewSet("./testData")
+var parseSet = NewSet(nil, "./testData")
type ParserTestCase struct {
*testing.T
@@ -79,9 +79,15 @@ func TestParseTemplateExpressions(t *testing.T) {
func TestParseTemplateBlockYield(t *testing.T) {
p := ParserTestCase{t}
p.TestPrintFile("block_yield.jet")
+ p.TestPrintFile("new_block_yield.jet")
}
func TestParseTemplateIndexSliceExpression(t *testing.T) {
p := ParserTestCase{t}
p.TestPrintFile("index_slice_expression.jet")
}
+
+func TestParseTemplateAssignment(t *testing.T) {
+ p := ParserTestCase{t}
+ p.TestPrintFile("assignment.jet")
+}
diff --git a/template.go b/template.go
index bc27393..eb3aefc 100644
--- a/template.go
+++ b/template.go
@@ -9,6 +9,7 @@ import (
"path"
"path/filepath"
"reflect"
+ "strings"
"sync"
"text/template"
)
@@ -16,13 +17,14 @@ import (
// Set responsible to load and cache templates, also holds some runtime data
// passed to Runtime at evaluating time.
type Set struct {
- dirs []string // directories for look to template files
- templates map[string]*Template // parsed templates
- escapee SafeWriter // escapee to use at runtime
- globals VarMap // global scope for this template set
- tmx sync.RWMutex // template parsing mutex
- gmx sync.RWMutex // global variables map mutex
- developmentMode bool
+ dirs []string // directories for look to template files
+ templates map[string]*Template // parsed templates
+ escapee SafeWriter // escapee to use at runtime
+ globals VarMap // global scope for this template set
+ tmx *sync.RWMutex // template parsing mutex
+ gmx *sync.RWMutex // global variables map mutex
+ defaultExtensions []string
+ developmentMode bool
}
// SetDevelopmentMode set's development mode on/off, in development mode template will be recompiled on every run
@@ -31,32 +33,36 @@ func (s *Set) SetDevelopmentMode(b bool) *Set {
return s
}
+func (a *Set) LookupGlobal(key string) (val interface{}, found bool) {
+ a.gmx.RLock()
+ val, found = a.globals[key]
+ a.gmx.RUnlock()
+ return
+}
+
// AddGlobal add or set a global variable into the Set
-func (s *Set) AddGlobal(key string, i interface{}) (val interface{}, override bool) {
+func (s *Set) AddGlobal(key string, i interface{}) *Set {
s.gmx.Lock()
if s.globals == nil {
s.globals = make(VarMap)
- } else {
- val, override = s.globals[key]
}
s.globals[key] = reflect.ValueOf(i)
s.gmx.Unlock()
- return
+ return s
+}
+
+func (s *Set) AddGlobalFunc(key string, fn Func) *Set {
+ return s.AddGlobal(key, fn)
}
// NewSet creates a new set, dir specifies a list of directories entries to search for templates
-func NewSet(dir ...string) *Set {
- return &Set{dirs: dir, templates: make(map[string]*Template)}
+func NewSet(escapee SafeWriter, dir ...string) *Set {
+ return &Set{dirs: dir, tmx: &sync.RWMutex{}, gmx: &sync.RWMutex{}, escapee: escapee, templates: make(map[string]*Template), defaultExtensions: append([]string{}, defaultExtensions...)}
}
// NewHTMLSet creates a new set, dir specifies a list of directories entries to search for templates
func NewHTMLSet(dir ...string) *Set {
- return &Set{dirs: dir, escapee: template.HTMLEscape, templates: make(map[string]*Template)}
-}
-
-// NewSafeSet creates a new set, dir specifies a list of directories entries to search for templates
-func NewSafeSet(escapee SafeWriter, dir ...string) *Set {
- return &Set{dirs: dir, escapee: escapee, templates: make(map[string]*Template)}
+ return NewSet(template.HTMLEscape, dir...)
}
// AddPath add path to the lookup list, when loading a template the Set will
@@ -70,8 +76,7 @@ func (s *Set) AddPath(path string) {
func (s *Set) AddGopathPath(path string) {
paths := filepath.SplitList(os.Getenv("GOPATH"))
for i := 0; i < len(paths); i++ {
- path, err := filepath.Abs(filepath.Join(paths[i], path))
-
+ path, err := filepath.Abs(filepath.Join(paths[i], "src", path))
if err != nil {
panic(errors.New("Can't add this path err: " + err.Error()))
}
@@ -87,102 +92,243 @@ func (s *Set) AddGopathPath(path string) {
}
}
-// load loads the template by name, if content is provided template Set will not
-// look in the file system and will parse the content string
-func (s *Set) load(name, content string) (template *Template, err error) {
- if content == "" {
- for i := 0; i < len(s.dirs); i++ {
- fileName := path.Join(s.dirs[i], name)
- var bytestring []byte
- bytestring, err = ioutil.ReadFile(fileName)
- if err == nil {
- content = string(bytestring)
- break
- }
+// fileExists checks if the template name exists by walking the list of template paths
+// returns string with the full path of the template and bool true if the template file was found
+func (s *Set) fileExists(name string) (string, bool) {
+ for i := 0; i < len(s.dirs); i++ {
+ fileName := path.Join(s.dirs[i], name)
+ if _, err := os.Stat(fileName); err == nil {
+ return fileName, true
}
- if content == "" && err != nil {
+ }
+ return "", false
+}
+
+// resolveName try to resolve a template name, the steps as follow
+// 1. try provided path
+// 2. try provided path+defaultExtensions
+// ex: set.resolveName("catalog/products.list") with defaultExtensions set to []string{".html.jet",".jet"}
+// try catalog/products.list
+// try catalog/products.list.html.jet
+// try catalog/products.list.jet
+func (s *Set) resolveName(name string) (newName, fileName string, foundLoaded, foundFile bool) {
+ newName = name
+ if _, foundLoaded = s.templates[newName]; foundLoaded {
+ return
+ }
+
+ if fileName, foundFile = s.fileExists(name); foundFile {
+ return
+ }
+
+ for _, extension := range s.defaultExtensions {
+ newName = name + extension
+ if _, foundLoaded = s.templates[newName]; foundLoaded {
+ return
+ }
+ if fileName, foundFile = s.fileExists(newName); foundFile {
return
}
}
- template, err = s.parse(name, content)
return
}
-// loadTemplate is used to load a template while parsing a template,
-// this function is not thread safe, the lock usually is called before by the parent function
-func (s *Set) loadTemplate(name, content string) (template *Template, err error) {
+func (s *Set) resolveNameSibling(name, sibling string) (newName, fileName string, foundLoaded, foundFile, isRelativeName bool) {
+ if sibling != "" {
+ i := strings.LastIndex(sibling, "/")
+ if i != -1 {
+ if newName, fileName, foundLoaded, foundFile = s.resolveName(path.Join(sibling[:i+1], name)); foundFile || foundLoaded {
+ isRelativeName = true
+ return
+ }
+ }
+ }
+ newName, fileName, foundLoaded, foundFile = s.resolveName(name)
+ return
+}
+
+// Parse parses the template, this method will link the template to the set but not the set to
+func (s *Set) Parse(name, content string) (*Template, error) {
+ sc := *s
+ sc.developmentMode = true
+
+ sc.tmx.RLock()
+ t, err := sc.parse(name, content)
+ sc.tmx.RUnlock()
+
+ return t, err
+}
+
+func (s *Set) loadFromFile(name, fileName string) (template *Template, err error) {
+ var content []byte
+ if content, err = ioutil.ReadFile(fileName); err == nil {
+ template, err = s.parse(name, string(content))
+ }
+ return
+}
+
+func (s *Set) getTemplateWhileParsing(parentName, name string) (template *Template, err error) {
+ name = path.Clean(name)
+
if s.developmentMode {
- template, err = s.load(name, content)
+ if newName, fileName, foundLoaded, foundPath, _ := s.resolveNameSibling(name, parentName); foundPath {
+ template, err = s.loadFromFile(newName, fileName)
+ } else if foundLoaded {
+ template = s.templates[newName]
+ } else {
+ err = fmt.Errorf("template %s can't be loaded", name)
+ }
return
}
- var ok bool
- if template, ok = s.templates[name]; ok {
- return
+ if newName, fileName, foundLoaded, foundPath, isRelative := s.resolveNameSibling(name, parentName); foundPath {
+ template, err = s.loadFromFile(newName, fileName)
+ s.templates[newName] = template
+
+ if !isRelative {
+ s.templates[name] = template
+ }
+ } else if foundLoaded {
+ template = s.templates[newName]
+ if !isRelative && name != newName {
+ s.templates[name] = template
+ }
+ } else {
+ err = fmt.Errorf("template %s can't be loaded", name)
}
- template, err = s.load(name, content)
- s.templates[name] = template
return
}
// getTemplate gets a template already loaded by name
-func (s *Set) getTemplate(name string) (template *Template, ok bool) {
+func (s *Set) getTemplate(name, sibling string) (template *Template, err error) {
+ name = path.Clean(name)
+
if s.developmentMode {
- template, _ = s.GetTemplate(name)
- ok = template != nil
+ s.tmx.RLock()
+ defer s.tmx.RUnlock()
+ if newName, fileName, foundLoaded, foundFile, _ := s.resolveNameSibling(name, sibling); foundFile || foundLoaded {
+ if foundFile {
+ template, err = s.loadFromFile(newName, fileName)
+ } else {
+ template, _ = s.templates[newName]
+ }
+ } else {
+ err = fmt.Errorf("template %s can't be loaded", name)
+ }
return
}
+
+ //fast path
s.tmx.RLock()
- template, ok = s.templates[name]
+ newName, fileName, foundLoaded, foundFile, isRelative := s.resolveNameSibling(name, sibling)
+
+ if foundLoaded {
+ template = s.templates[newName]
+ s.tmx.RUnlock()
+ if !isRelative && name != newName {
+ // creates an alias
+ s.tmx.Lock()
+ if _, found := s.templates[name]; !found {
+ s.templates[name] = template
+ }
+ s.tmx.Unlock()
+ }
+ return
+ }
s.tmx.RUnlock()
+
+ //not found parses and cache
+ s.tmx.Lock()
+ defer s.tmx.Unlock()
+
+ newName, fileName, foundLoaded, foundFile, isRelative = s.resolveNameSibling(name, sibling)
+ if foundLoaded {
+ template = s.templates[newName]
+ if !isRelative && name != newName {
+ // creates an alias
+ if _, found := s.templates[name]; !found {
+ s.templates[name] = template
+ }
+ }
+ } else if foundFile {
+ template, err = s.loadFromFile(newName, fileName)
+
+ if !isRelative && name != newName {
+ // creates an alias
+ if _, found := s.templates[name]; !found {
+ s.templates[name] = template
+ }
+ }
+
+ s.templates[newName] = template
+ } else {
+ err = fmt.Errorf("template %s can't be loaded", name)
+ }
return
}
-// GetTemplate calls LoadTemplate and returns the template, template is already loaded return it, if
-// not load, cache and return
-func (s *Set) GetTemplate(name string) (*Template, error) {
- return s.LoadTemplate(name, "")
+func (s *Set) GetTemplate(name string) (template *Template, err error) {
+ template, err = s.getTemplate(name, "")
+ return
}
-// LoadTemplate loads a template by name, and caches the template in the set, if content is provided
-// content will be parsed instead of file
func (s *Set) LoadTemplate(name, content string) (template *Template, err error) {
if s.developmentMode {
- template, err = s.load(name, content)
+ s.tmx.RLock()
+ defer s.tmx.RUnlock()
+ template, err = s.parse(name, content)
return
}
- var ok bool
-
+ //fast path
+ var found bool
s.tmx.RLock()
- if template, ok = s.templates[name]; ok {
+ if template, found = s.templates[name]; found {
s.tmx.RUnlock()
return
}
-
s.tmx.RUnlock()
+
+ //not found parses and cache
s.tmx.Lock()
defer s.tmx.Unlock()
- template, ok = s.templates[name]
- if ok && template != nil {
+ if template, found = s.templates[name]; found {
return
}
- template, err = s.load(name, content)
- s.templates[name] = template // saves the template
+ if template, err = s.parse(name, content); err == nil {
+ s.templates[name] = template
+ }
+
return
}
func (t *Template) String() (template string) {
if t.extends != nil {
- template += fmt.Sprintf("{{extends %q}}", t.extends.ParseName)
+ if len(t.root.Nodes) > 0 && len(t.imports) == 0 {
+ template += fmt.Sprintf("{{extends %q}}", t.extends.ParseName)
+ } else {
+ template += fmt.Sprintf("{{extends %q}}", t.extends.ParseName)
+ }
+ }
+
+ for k, _import := range t.imports {
+ if t.extends == nil && k == 0 {
+ template += fmt.Sprintf("{{import %q}}", _import.ParseName)
+ } else {
+ template += fmt.Sprintf("\n{{import %q}}", _import.ParseName)
+ }
}
- for _, _import := range t.imports {
- template += fmt.Sprintf("\n{{import %q}}", _import.ParseName)
+
+ if t.extends != nil || len(t.imports) > 0 {
+ if len(t.root.Nodes) > 0 {
+ template += "\n" + t.root.String()
+ }
+ } else {
+ template += t.root.String()
}
- template += t.root.String()
return
}
@@ -199,23 +345,39 @@ func (t *Template) addBlocks(blocks map[string]*BlockNode) {
type VarMap map[string]reflect.Value
-func (scope VarMap) Set(name string, v interface{}) {
+func (scope VarMap) Set(name string, v interface{}) VarMap {
+ scope[name] = reflect.ValueOf(v)
+ return scope
+}
+
+func (scope VarMap) SetFunc(name string, v Func) VarMap {
scope[name] = reflect.ValueOf(v)
+ return scope
+}
+
+func (scope VarMap) SetWriter(name string, v SafeWriter) VarMap {
+ scope[name] = reflect.ValueOf(v)
+ return scope
}
// Execute executes the template in the w Writer
-func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) (err error) {
+func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) error {
+ return t.ExecuteI18N(nil, w, variables, data)
+}
+
+type Translator interface {
+ Msg(key, defaultValue string) string
+ Trans(format, defaultFormat string, v ...interface{}) string
+}
+
+func (t *Template) ExecuteI18N(translator Translator, w io.Writer, variables VarMap, data interface{}) (err error) {
st := pool_State.Get().(*Runtime)
defer st.recover(&err)
- if data != nil {
- st.context = reflect.ValueOf(data)
- }
-
st.blocks = t.processedBlocks
- st.set = t.set
-
+ st.translator = translator
st.variables = variables
+ st.set = t.set
st.Writer = w
// resolve extended template
@@ -223,7 +385,10 @@ func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) (err
t = t.extends
}
- // execute the extended root
+ if data != nil {
+ st.context = reflect.ValueOf(data)
+ }
+
st.executeList(t.root)
return
}
diff --git a/testData/assignment.jet b/testData/assignment.jet
new file mode 100644
index 0000000..7b2069d
--- /dev/null
+++ b/testData/assignment.jet
@@ -0,0 +1,9 @@
+{{ newURL := url("","").Method(""); newURL |pipe }}
+{{ newName := name; safeHtml: newName, " ", "new name" }}
+{{ newName,newValue := name,value }}
+{{ value,found := name["key"] }}
+===
+{{newURL:=url("", "").Method("");newURL | pipe}}
+{{newName:=name;safeHtml:newName, " ", "new name"}}
+{{newName, newValue:=name, value}}
+{{value, found:=name["key"]}}
\ No newline at end of file
diff --git a/testData/block_yield.jet b/testData/block_yield.jet
index 97aa800..574e95a 100644
--- a/testData/block_yield.jet
+++ b/testData/block_yield.jet
@@ -1,17 +1,17 @@
{{ extends "base.jet" }}
-{{ block mainMenu }}{{ end }}
-{{ block mainContent}}
- {{ yield mainMenu pipeValue}}
- {{ block subContent request.Post("Name") }} {{ end }}
+{{ block mainMenu() }}{{ end }}
+{{ block mainContent() }}
+ {{ yield mainMenu() pipeValue}}
+ {{ block subContent() request.Post("Name") }} {{ end }}
{{ include "include.jet" }}
{{ end }}
{{ include "include.jet" }}
-
===
-{{extends "base.jet"}}{{block mainMenu}}{{end}}
-{{block mainContent}}
- {{yield mainMenu pipeValue}}
- {{block subContent request.Post("Name")}} {{end}}
+{{extends "base.jet"}}
+{{block mainMenu()}}{{end}}
+{{block mainContent()}}
+ {{yield mainMenu() pipeValue}}
+ {{block subContent() request.Post("Name")}} {{end}}
{{include "include.jet"}}
{{end}}
-{{include "include.jet"}}
\ No newline at end of file
+{{include "include.jet"}}
diff --git a/testData/imports.jet b/testData/imports.jet
index 9991e70..7ad87e3 100644
--- a/testData/imports.jet
+++ b/testData/imports.jet
@@ -4,4 +4,4 @@
===
{{extends "base.jet"}}
{{import "library.jet"}}
-{{import "library.jet"}}
\ No newline at end of file
+{{import "library.jet"}}
diff --git a/testData/new_block_yield.jet b/testData/new_block_yield.jet
new file mode 100644
index 0000000..54b18cd
--- /dev/null
+++ b/testData/new_block_yield.jet
@@ -0,0 +1,54 @@
+{{ extends "base.jet" }}
+{{ block textfield(label,name,value) }}
+ {{label}}:
+{{ end }}
+
+{{ block col(md=12,offset=0) }}
+ {{ yield content }}
+{{ end }}
+
+{{ block row() .}}
+ {{ yield content }}
+{{ content }}
+
+
+
+{{ end }}
+
+{{ block header() }}
+ {{ yield row() content}}
+ {{ yield col(md=6) content }}
+{{ yield content }}
+ {{end}}
+ {{end}}
+{{ end }}
+
+{{ include "include.jet" }}
+
+===
+{{extends "base.jet"}}
+{{block textfield(label,name,value)}}
+ {{label}}:
+{{end}}
+
+{{block col(md=12,offset=0)}}
+ {{yield content}}
+{{end}}
+
+{{block row() .}}
+ {{yield content}}
+{{content}}
+
+
+
+{{end}}
+
+{{block header()}}
+ {{yield row() content}}
+ {{yield col(md=6) content}}
+{{yield content}}
+ {{end}}
+ {{end}}
+{{end}}
+
+{{include "include.jet"}}
diff --git a/testData/resolve/extension.jet.html b/testData/resolve/extension.jet.html
new file mode 100644
index 0000000..0c5669f
--- /dev/null
+++ b/testData/resolve/extension.jet.html
@@ -0,0 +1 @@
+extension.jet.html
\ No newline at end of file
diff --git a/testData/resolve/simple b/testData/resolve/simple
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/testData/resolve/simple
@@ -0,0 +1 @@
+simple
\ No newline at end of file
diff --git a/testData/resolve/simple.jet b/testData/resolve/simple.jet
new file mode 100644
index 0000000..0d3d396
--- /dev/null
+++ b/testData/resolve/simple.jet
@@ -0,0 +1 @@
+simple.jet
\ No newline at end of file
diff --git a/testData/resolve/sub/extend b/testData/resolve/sub/extend
new file mode 100644
index 0000000..eaf0134
--- /dev/null
+++ b/testData/resolve/sub/extend
@@ -0,0 +1 @@
+{{extends "subextend"}}
\ No newline at end of file
diff --git a/testData/resolve/sub/subextend b/testData/resolve/sub/subextend
new file mode 100644
index 0000000..dfd417f
--- /dev/null
+++ b/testData/resolve/sub/subextend
@@ -0,0 +1 @@
+{{include "../simple"}} - {{include "../simple.jet"}} - {{include "../extension"}}
\ No newline at end of file
diff --git a/testData/simple_expression.jet b/testData/simple_expression.jet
index 80a80d9..48d4c91 100644
--- a/testData/simple_expression.jet
+++ b/testData/simple_expression.jet
@@ -8,8 +8,6 @@
{{ url("") |pipe |pipe }}
{{ url("","").Field |pipe }}
{{ url("","").Method("") |pipe }}
-{{ newURL:=url("","").Method(""); newURL |pipe }}
-{{ newName := name; safeHtml: newName, " ", "new name" }}
===
{{.}}
{{singleValue}}
@@ -20,6 +18,4 @@
{{url:"", "" | pipe}}
{{url("") | pipe | pipe}}
{{url("", "").Field | pipe}}
-{{url("", "").Method("") | pipe}}
-{{newURL:=url("", "").Method("");newURL | pipe}}
-{{newName:=name;safeHtml:newName, " ", "new name"}}
\ No newline at end of file
+{{url("", "").Method("") | pipe}}
\ No newline at end of file