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}} -
-
{{.Title}}
-
{{.Description}} - Read more
-
- {{end}} -{{end}} + $ cd example; go run main.go ``` -#### Import Extends Yield - -```HTML -{{extends "common/layout.jet"}} -{{import "common/menu.jet"}} - -{{block Header}} - - -{{end}} - -{{block Content}} - {{.PageHeader}} - - {{range .Result}} -
-
{{.Title}}
-
{{.Description}} - Read more
-
- {{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{{ yield row() content}}\n\t\t{{ yield col(md=6) content }}\n{{ yield content }}\n\t\t{{end}}\n\t{{end}}\n
\n\t\t{{content}}\n

Hey

\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{{ yield row() content}}\n\t\t{{ yield col(md=6) content }}\n{{ yield content }}\n\t\t{{end}}\n\t{{end}}\n
\n\t\t{{content}}\n

Hey

\n\t\t{{ end }}", + "\n
\n\t\t\n\t\t\n
\n
\n
\n
\n\t\t
\n\t\t\n\t\t\n
\n\t\n
\n\t\t\n
\n\n

Hey

\n\t\t\n\t\t
\n\t\t\n\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 \n \n
12
\n\n \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()}} +
    + {{yield content}} +
+{{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