├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── cache.go ├── constructors.go ├── default.go ├── docs ├── builtins.md ├── changes.md ├── index.md └── syntax.md ├── dump.go ├── dump_test.go ├── eval.go ├── eval_test.go ├── examples ├── asset_packaging │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── assets │ │ ├── generate.go │ │ └── templates │ │ │ └── templates.go │ ├── main.go │ └── views │ │ ├── includes │ │ ├── _partial.jet │ │ └── blocks.jet │ │ ├── index.jet │ │ └── layouts │ │ └── application.jet └── todos │ ├── devop.yml │ ├── main.go │ └── views │ ├── layouts │ └── application.jet │ └── todos │ ├── index.jet │ └── show.jet ├── exec.go ├── exec_test.go ├── func.go ├── go.mod ├── go.sum ├── jettest └── test.go ├── lex.go ├── lex_test.go ├── loader.go ├── loaders ├── embedfs │ ├── embedfs_test.go │ ├── loader.go │ └── testData │ │ └── includeIfNotExists │ │ ├── existent.jet │ │ ├── exists.jet │ │ ├── ifIncludeIfExits.jet │ │ ├── notExistent.jet │ │ ├── wcontext.jet │ │ └── wcontext_child.jet ├── httpfs │ ├── httpfs_test.go │ ├── loader.go │ └── testData │ │ └── includeIfNotExists │ │ ├── existent.jet │ │ ├── exists.jet │ │ ├── ifIncludeIfExits.jet │ │ ├── notExistent.jet │ │ ├── wcontext.jet │ │ └── wcontext_child.jet └── multi │ ├── multi.go │ ├── multi_test.go │ └── testData │ └── simple2.jet ├── node.go ├── parse.go ├── parse_test.go ├── profile.sh ├── ranger.go ├── ranger_test.go ├── set.go ├── set_test.go ├── stress.bash ├── testData ├── additive_expression.jet ├── assignment.jet ├── base.jet ├── block_yield.jet ├── custom_delimiters.jet ├── devdump.jet ├── execReturn │ ├── in_if.jet │ ├── in_include.jet │ ├── in_range.jet │ ├── included.jet │ ├── simple.jet │ ├── test_in_if.jet │ ├── test_in_include.jet │ ├── test_in_range.jet │ └── test_simple.jet ├── extends.jet ├── if.jet ├── imports.jet ├── includeIfNotExists │ ├── broken.jet │ ├── existent.jet │ ├── exists.jet │ ├── ifIncludeIfExits.jet │ ├── includeBroken.jet │ ├── notExistent.jet │ ├── wcontext.jet │ └── wcontext_child.jet ├── index_slice_expression.jet ├── library.jet ├── multiplicative_expression.jet ├── new_block_yield.jet ├── range.jet ├── resolve │ ├── extension.jet.html │ ├── simple │ ├── simple.jet │ └── sub │ │ ├── extend │ │ └── subextend ├── simple_expression.jet ├── tryCatch │ ├── panic.jet │ ├── try.jet │ ├── try_catch.jet │ ├── try_catch_err.jet │ └── try_include.jet └── whitespaceControl │ ├── multiple.jet │ └── simple.jet └── utils ├── visitor.go └── visitor_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.13.x" 4 | - "1.14.x" 5 | - "1.15.x" 6 | - "tip" 7 | 8 | script: 9 | - env GO111MODULE=on go test -v ./... 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jet Template Engine for Go 2 | 3 | [![Build Status](https://travis-ci.org/CloudyKit/jet.svg?branch=master)](https://travis-ci.org/CloudyKit/jet) [![Build status](https://ci.appveyor.com/api/projects/status/5g4whw3c6518vvku?svg=true)](https://ci.appveyor.com/project/CloudyKit/jet) [![Join the chat at https://gitter.im/CloudyKit/jet](https://badges.gitter.im/CloudyKit/jet.svg)](https://gitter.im/CloudyKit/jet) 4 | 5 | Jet is a template engine developed to be easy to use, powerful, dynamic, yet secure and very fast. 6 | 7 | * simple and familiar syntax 8 | * supports template inheritance (`extends`) and composition (`block`/`yield`, `import`, `include`) 9 | * descriptive error messages with filename and line number 10 | * auto-escaping 11 | * simple C-like expressions 12 | * very fast execution – Jet can execute templates faster than some pre-compiled template engines 13 | * very light in terms of allocations and memory footprint 14 | 15 | ## v6 16 | 17 | Version 6 brings major improvements to the Go API. Make sure to read through the [breaking changes](./docs/changes.md) before making the jump. 18 | 19 | ## Docs 20 | 21 | - [Go API](https://pkg.go.dev/github.com/CloudyKit/jet/v6#section-documentation) 22 | - [Syntax Reference](./docs/syntax.md) 23 | - [Built-ins](./docs/builtins.md) 24 | - [Wiki](https://github.com/CloudyKit/jet/wiki) (some things are out of date) 25 | 26 | ## Example application 27 | 28 | An example to-do application is available in [examples/todos](./examples/todos). Clone the repository, then (in the repository root) do: 29 | ``` 30 | $ cd examples/todos; go run main.go 31 | ``` 32 | 33 | ## IntelliJ Plugin 34 | 35 | If you use IntelliJ there is a plugin available at https://github.com/jhsx/GoJetPlugin. 36 | There is also a very good Go plugin for IntelliJ – see https://github.com/go-lang-plugin-org/go-lang-idea-plugin. 37 | GoJetPlugin + Go-lang-idea-plugin = happiness! 38 | 39 | ## Contributing 40 | 41 | All contributions are welcome – if you find a bug please report it. 42 | 43 | ## Contributors 44 | 45 | - José Santos (@jhsx) 46 | - Daniel Lohse (@annismckenzie) 47 | - Alexander Willing (@sauerbraten) 48 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | image: Visual Studio 2019 4 | 5 | # scripts that are called at very beginning, before repo cloning 6 | init: 7 | - git config --global core.autocrlf true 8 | 9 | clone_folder: c:\gopath\src\github.com\CloudyKit\jet 10 | 11 | environment: 12 | GOPATH: c:\gopath 13 | GO111MODULE: on 14 | matrix: 15 | - GOVERSION: 113 16 | - GOVERSION: 114 17 | - GOVERSION: 115 18 | 19 | install: 20 | - set PATH=%GOPATH%\bin;c:\go%GOVERSION%\bin;%PATH% 21 | - set GOROOT=c:\go%GOVERSION% 22 | - echo %PATH% 23 | - echo %GOPATH% 24 | - go version 25 | - go env 26 | 27 | build: off 28 | 29 | test_script: 30 | - go test -v ./... 31 | - cd examples/asset_packaging/ 32 | - go generate 33 | - go run -tags=deploy_build main.go --run-and-exit 34 | - go build -tags=deploy_build -o bin/app.exe main.go 35 | - .\bin\app.exe --run-and-exit 36 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package jet 2 | 3 | import "sync" 4 | 5 | // Cache is the interface Jet uses to store and retrieve parsed templates. 6 | type Cache interface { 7 | 8 | // Get fetches a template from the cache. If Get returns nil, the same path with a different extension will be tried. 9 | // If Get() returns nil for all configured extensions, the same path and extensions will be tried on the Set's Loader. 10 | Get(templatePath string) *Template 11 | 12 | // Put places the result of parsing a template "file"/string in the cache. 13 | Put(templatePath string, t *Template) 14 | } 15 | 16 | // cache is the cache used by default in a new Set. 17 | type cache struct { 18 | m sync.Map 19 | } 20 | 21 | // compile-time check that cache implements Cache 22 | var _ Cache = (*cache)(nil) 23 | 24 | func (c *cache) Get(templatePath string) *Template { 25 | _t, ok := c.m.Load(templatePath) 26 | if !ok { 27 | return nil 28 | } 29 | return _t.(*Template) 30 | } 31 | 32 | func (c *cache) Put(templatePath string, t *Template) { 33 | c.m.Store(templatePath, t) 34 | } 35 | -------------------------------------------------------------------------------- /constructors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | "strings" 21 | ) 22 | 23 | func (t *Template) newSliceExpr(pos Pos, line int, base, index, endIndex Expression) *SliceExprNode { 24 | return &SliceExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeSliceExpr, Pos: pos, Line: line}, Index: index, Base: base, EndIndex: endIndex} 25 | } 26 | 27 | func (t *Template) newIndexExpr(pos Pos, line int, base, index Expression) *IndexExprNode { 28 | return &IndexExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeIndexExpr, Pos: pos, Line: line}, Index: index, Base: base} 29 | } 30 | 31 | func (t *Template) newTernaryExpr(pos Pos, line int, boolean, left, right Expression) *TernaryExprNode { 32 | return &TernaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeTernaryExpr, Pos: pos, Line: line}, Boolean: boolean, Left: left, Right: right} 33 | } 34 | 35 | func (t *Template) newSet(pos Pos, line int, isLet, isIndexExprGetLookup bool, left, right []Expression) *SetNode { 36 | return &SetNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeSet, Pos: pos, Line: line}, Let: isLet, IndexExprGetLookup: isIndexExprGetLookup, Left: left, Right: right} 37 | } 38 | 39 | func (t *Template) newCallExpr(pos Pos, line int, expr Expression) *CallExprNode { 40 | return &CallExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeCallExpr, Pos: pos, Line: line}, BaseExpr: expr} 41 | } 42 | func (t *Template) newNotExpr(pos Pos, line int, expr Expression) *NotExprNode { 43 | return &NotExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNotExpr, Pos: pos, Line: line}, Expr: expr} 44 | } 45 | func (t *Template) newNumericComparativeExpr(pos Pos, line int, left, right Expression, item item) *NumericComparativeExprNode { 46 | return &NumericComparativeExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNumericComparativeExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}} 47 | } 48 | 49 | func (t *Template) newComparativeExpr(pos Pos, line int, left, right Expression, item item) *ComparativeExprNode { 50 | return &ComparativeExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeComparativeExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}} 51 | } 52 | 53 | func (t *Template) newLogicalExpr(pos Pos, line int, left, right Expression, item item) *LogicalExprNode { 54 | return &LogicalExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeLogicalExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}} 55 | } 56 | 57 | func (t *Template) newMultiplicativeExpr(pos Pos, line int, left, right Expression, item item) *MultiplicativeExprNode { 58 | return &MultiplicativeExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeMultiplicativeExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}} 59 | } 60 | 61 | func (t *Template) newAdditiveExpr(pos Pos, line int, left, right Expression, item item) *AdditiveExprNode { 62 | return &AdditiveExprNode{binaryExprNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeAdditiveExpr, Pos: pos, Line: line}, Operator: item, Left: left, Right: right}} 63 | } 64 | 65 | func (t *Template) newList(pos Pos) *ListNode { 66 | return &ListNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeList, Pos: pos}} 67 | } 68 | 69 | func (t *Template) newText(pos Pos, text string) *TextNode { 70 | return &TextNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeText, Pos: pos}, Text: []byte(text)} 71 | } 72 | 73 | func (t *Template) newPipeline(pos Pos, line int) *PipeNode { 74 | return &PipeNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodePipe, Pos: pos, Line: line}} 75 | } 76 | 77 | func (t *Template) newAction(pos Pos, line int) *ActionNode { 78 | return &ActionNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeAction, Pos: pos, Line: line}} 79 | } 80 | 81 | func (t *Template) newCommand(pos Pos) *CommandNode { 82 | return &CommandNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeCommand, Pos: pos}} 83 | } 84 | 85 | func (t *Template) newNil(pos Pos) *NilNode { 86 | return &NilNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNil, Pos: pos}} 87 | } 88 | 89 | func (t *Template) newField(pos Pos, line int, ident string) *FieldNode { 90 | return &FieldNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeField, Pos: pos, Line: line}, Ident: strings.Split(ident[1:], ".")} //[1:] to drop leading period 91 | } 92 | 93 | func (t *Template) newChain(pos Pos, line int, node Node) *ChainNode { 94 | return &ChainNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeChain, Pos: pos, Line: line}, Node: node} 95 | } 96 | 97 | func (t *Template) newBool(pos Pos, true bool) *BoolNode { 98 | return &BoolNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeBool, Pos: pos}, True: true} 99 | } 100 | 101 | func (t *Template) newString(pos Pos, orig, text string) *StringNode { 102 | return &StringNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeString, Pos: pos}, Quoted: orig, Text: text} 103 | } 104 | 105 | func (t *Template) newEnd(pos Pos) *endNode { 106 | return &endNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeEnd, Pos: pos}} 107 | } 108 | 109 | func (t *Template) newContent(pos Pos) *contentNode { 110 | return &contentNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeContent, Pos: pos}} 111 | } 112 | 113 | func (t *Template) newElse(pos Pos, line int) *elseNode { 114 | return &elseNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeElse, Pos: pos, Line: line}} 115 | } 116 | 117 | func (t *Template) newIf(pos Pos, line int, set *SetNode, pipe Expression, list, elseList *ListNode) *IfNode { 118 | return &IfNode{BranchNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeIf, Pos: pos, Line: line}, Set: set, Expression: pipe, List: list, ElseList: elseList}} 119 | } 120 | 121 | func (t *Template) newRange(pos Pos, line int, set *SetNode, pipe Expression, list, elseList *ListNode) *RangeNode { 122 | return &RangeNode{BranchNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeRange, Pos: pos, Line: line}, Set: set, Expression: pipe, List: list, ElseList: elseList}} 123 | } 124 | 125 | func (t *Template) newBlock(pos Pos, line int, name string, parameters *BlockParameterList, pipe Expression, listNode, contentListNode *ListNode) *BlockNode { 126 | return &BlockNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeBlock, Line: line, Pos: pos}, Name: name, Parameters: parameters, Expression: pipe, List: listNode, Content: contentListNode} 127 | } 128 | 129 | func (t *Template) newYield(pos Pos, line int, name string, bplist *BlockParameterList, pipe Expression, content *ListNode, isContent bool) *YieldNode { 130 | return &YieldNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeYield, Pos: pos, Line: line}, Name: name, Parameters: bplist, Expression: pipe, Content: content, IsContent: isContent} 131 | } 132 | 133 | func (t *Template) newInclude(pos Pos, line int, name, context Expression) *IncludeNode { 134 | return &IncludeNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeInclude, Pos: pos, Line: line}, Name: name, Context: context} 135 | } 136 | 137 | func (t *Template) newReturn(pos Pos, line int, pipe Expression) *ReturnNode { 138 | return &ReturnNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeReturn, Pos: pos, Line: line}, Value: pipe} 139 | } 140 | 141 | func (t *Template) newTry(pos Pos, line int, list *ListNode, catch *catchNode) *TryNode { 142 | return &TryNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeTry, Pos: pos, Line: line}, List: list, Catch: catch} 143 | } 144 | 145 | func (t *Template) newCatch(pos Pos, line int, errVar *IdentifierNode, list *ListNode) *catchNode { 146 | return &catchNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: nodeCatch, Pos: pos, Line: line}, Err: errVar, List: list} 147 | } 148 | 149 | func (t *Template) newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) { 150 | n := &NumberNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeNumber, Pos: pos}, Text: text} 151 | // todo: optimize 152 | switch typ { 153 | case itemCharConstant: 154 | _rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0]) 155 | if err != nil { 156 | return nil, err 157 | } 158 | if tail != "'" { 159 | return nil, fmt.Errorf("malformed character constant: %s", text) 160 | } 161 | n.Int64 = int64(_rune) 162 | n.IsInt = true 163 | n.Uint64 = uint64(_rune) 164 | n.IsUint = true 165 | n.Float64 = float64(_rune) //odd but those are the rules. 166 | n.IsFloat = true 167 | return n, nil 168 | case itemComplex: 169 | //fmt.Sscan can parse the pair, so let it do the work. 170 | if _, err := fmt.Sscan(text, &n.Complex128); err != nil { 171 | return nil, err 172 | } 173 | n.IsComplex = true 174 | n.simplifyComplex() 175 | return n, nil 176 | } 177 | //Imaginary constants can only be complex unless they are zero. 178 | if len(text) > 0 && text[len(text)-1] == 'i' { 179 | f, err := strconv.ParseFloat(text[:len(text)-1], 64) 180 | if err == nil { 181 | n.IsComplex = true 182 | n.Complex128 = complex(0, f) 183 | n.simplifyComplex() 184 | return n, nil 185 | } 186 | } 187 | // Do integer test first so we get 0x123 etc. 188 | u, err := strconv.ParseUint(text, 0, 64) // will fail for -0; fixed below. 189 | if err == nil { 190 | n.IsUint = true 191 | n.Uint64 = u 192 | } 193 | i, err := strconv.ParseInt(text, 0, 64) 194 | if err == nil { 195 | n.IsInt = true 196 | n.Int64 = i 197 | if i == 0 { 198 | n.IsUint = true // in case of -0. 199 | n.Uint64 = u 200 | } 201 | } 202 | // If an integer extraction succeeded, promote the float. 203 | if n.IsInt { 204 | n.IsFloat = true 205 | n.Float64 = float64(n.Int64) 206 | } else if n.IsUint { 207 | n.IsFloat = true 208 | n.Float64 = float64(n.Uint64) 209 | } else { 210 | f, err := strconv.ParseFloat(text, 64) 211 | if err == nil { 212 | // If we parsed it as a float but it looks like an integer, 213 | // it's a huge number too large to fit in an int. Reject it. 214 | if !strings.ContainsAny(text, ".eE") { 215 | return nil, fmt.Errorf("integer overflow: %q", text) 216 | } 217 | n.IsFloat = true 218 | n.Float64 = f 219 | // If a floating-point extraction succeeded, extract the int if needed. 220 | if !n.IsInt && float64(int64(f)) == f { 221 | n.IsInt = true 222 | n.Int64 = int64(f) 223 | } 224 | if !n.IsUint && float64(uint64(f)) == f { 225 | n.IsUint = true 226 | n.Uint64 = uint64(f) 227 | } 228 | } 229 | } 230 | 231 | if !n.IsInt && !n.IsUint && !n.IsFloat { 232 | return nil, fmt.Errorf("illegal number syntax: %q", text) 233 | } 234 | 235 | return n, nil 236 | } 237 | 238 | func (t *Template) newIdentifier(ident string, pos Pos, line int) *IdentifierNode { 239 | return &IdentifierNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeIdentifier, Pos: pos, Line: line}, Ident: ident} 240 | } 241 | 242 | func (t *Template) newUnderscore(pos Pos, line int) *UnderscoreNode { 243 | return &UnderscoreNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeUnderscore, Pos: pos, Line: line}} 244 | } 245 | -------------------------------------------------------------------------------- /default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "html" 22 | "io" 23 | "io/ioutil" 24 | "net/url" 25 | "reflect" 26 | "strings" 27 | "text/template" 28 | ) 29 | 30 | var defaultVariables map[string]reflect.Value 31 | 32 | func init() { 33 | defaultVariables = map[string]reflect.Value{ 34 | "lower": reflect.ValueOf(strings.ToLower), 35 | "upper": reflect.ValueOf(strings.ToUpper), 36 | "hasPrefix": reflect.ValueOf(strings.HasPrefix), 37 | "hasSuffix": reflect.ValueOf(strings.HasSuffix), 38 | "repeat": reflect.ValueOf(strings.Repeat), 39 | "replace": reflect.ValueOf(strings.Replace), 40 | "split": reflect.ValueOf(strings.Split), 41 | "trimSpace": reflect.ValueOf(strings.TrimSpace), 42 | "html": reflect.ValueOf(html.EscapeString), 43 | "url": reflect.ValueOf(url.QueryEscape), 44 | "safeHtml": reflect.ValueOf(SafeWriter(template.HTMLEscape)), 45 | "safeJs": reflect.ValueOf(SafeWriter(template.JSEscape)), 46 | "raw": reflect.ValueOf(SafeWriter(unsafePrinter)), 47 | "unsafe": reflect.ValueOf(SafeWriter(unsafePrinter)), 48 | "writeJson": reflect.ValueOf(jsonRenderer), 49 | "json": reflect.ValueOf(json.Marshal), 50 | "map": reflect.ValueOf(newMap), 51 | "slice": reflect.ValueOf(newSlice), 52 | "array": reflect.ValueOf(newSlice), 53 | "isset": reflect.ValueOf(Func(func(a Arguments) reflect.Value { 54 | a.RequireNumOfArguments("isset", 1, -1) 55 | for i := 0; i < a.NumOfArguments(); i++ { 56 | if !a.IsSet(i) { 57 | return valueBoolFALSE 58 | } 59 | } 60 | return valueBoolTRUE 61 | })), 62 | "len": reflect.ValueOf(Func(func(a Arguments) reflect.Value { 63 | a.RequireNumOfArguments("len", 1, 1) 64 | 65 | expression := a.Get(0) 66 | if !expression.IsValid() { 67 | a.Panicf("len(): argument is not a valid value") 68 | } 69 | if expression.Kind() == reflect.Ptr || expression.Kind() == reflect.Interface { 70 | expression = expression.Elem() 71 | } 72 | 73 | switch expression.Kind() { 74 | case reflect.Array, reflect.Chan, reflect.Slice, reflect.Map, reflect.String: 75 | return reflect.ValueOf(expression.Len()) 76 | case reflect.Struct: 77 | return reflect.ValueOf(expression.NumField()) 78 | } 79 | 80 | a.Panicf("len(): invalid value type %s", expression.Type()) 81 | return reflect.Value{} 82 | })), 83 | "includeIfExists": reflect.ValueOf(Func(func(a Arguments) reflect.Value { 84 | a.RequireNumOfArguments("includeIfExists", 1, 2) 85 | t, err := a.runtime.set.GetTemplate(a.Get(0).String()) 86 | // If template exists but returns an error then panic instead of failing silently 87 | if t != nil && err != nil { 88 | panic(fmt.Errorf("including %s: %w", a.Get(0).String(), err)) 89 | } 90 | if err != nil { 91 | return hiddenFalse 92 | } 93 | 94 | a.runtime.newScope() 95 | defer a.runtime.releaseScope() 96 | 97 | a.runtime.blocks = t.processedBlocks 98 | root := t.Root 99 | if t.extends != nil { 100 | root = t.extends.Root 101 | } 102 | 103 | if a.NumOfArguments() > 1 { 104 | c := a.runtime.context 105 | defer func() { a.runtime.context = c }() 106 | a.runtime.context = a.Get(1) 107 | } 108 | 109 | a.runtime.executeList(root) 110 | 111 | return hiddenTrue 112 | })), 113 | "exec": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) { 114 | a.RequireNumOfArguments("exec", 1, 2) 115 | t, err := a.runtime.set.GetTemplate(a.Get(0).String()) 116 | if err != nil { 117 | panic(fmt.Errorf("exec(%s, %v): %w", a.Get(0), a.Get(1), err)) 118 | } 119 | 120 | a.runtime.newScope() 121 | defer a.runtime.releaseScope() 122 | 123 | w := a.runtime.Writer 124 | defer func() { a.runtime.Writer = w }() 125 | a.runtime.Writer = ioutil.Discard 126 | 127 | a.runtime.blocks = t.processedBlocks 128 | root := t.Root 129 | if t.extends != nil { 130 | root = t.extends.Root 131 | } 132 | 133 | if a.NumOfArguments() > 1 { 134 | c := a.runtime.context 135 | defer func() { a.runtime.context = c }() 136 | a.runtime.context = a.Get(1) 137 | } 138 | result = a.runtime.executeList(root) 139 | 140 | return result 141 | })), 142 | "ints": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) { 143 | var from, to int64 144 | err := a.ParseInto(&from, &to) 145 | if err != nil { 146 | panic(err) 147 | } 148 | // check to > from 149 | if to <= from { 150 | panic(errors.New("invalid range for ints ranger: 'from' must be smaller than 'to'")) 151 | } 152 | return reflect.ValueOf(newIntsRanger(from, to)) 153 | })), 154 | "dump": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) { 155 | switch numArgs := a.NumOfArguments(); numArgs { 156 | case 0: 157 | // no arguments were provided, dump all; do not recurse over parents 158 | return dumpAll(a, 0) 159 | case 1: 160 | if arg := a.Get(0); arg.Kind() == reflect.Float64 { 161 | // dump all, maybe walk into parents 162 | return dumpAll(a, int(arg.Float())) 163 | } 164 | fallthrough 165 | default: 166 | // one or more arguments were provided, grab them and check they are all strings 167 | ids := make([]string, numArgs) 168 | for i := range ids { 169 | arg := a.Get(i) 170 | if arg.Kind() != reflect.String { 171 | panic(fmt.Errorf("dump: expected argument %d to be a string, but got a %T", i, arg.Interface())) 172 | } 173 | ids = append(ids, arg.String()) 174 | } 175 | return dumpIdentified(a.runtime, ids) 176 | } 177 | })), 178 | } 179 | } 180 | 181 | type hiddenBool bool 182 | 183 | func (m hiddenBool) Render(r *Runtime) { /* render nothing -> hidden */ } 184 | 185 | var hiddenTrue = reflect.ValueOf(hiddenBool(true)) 186 | var hiddenFalse = reflect.ValueOf(hiddenBool(false)) 187 | 188 | func jsonRenderer(v interface{}) RendererFunc { 189 | return func(r *Runtime) { 190 | err := json.NewEncoder(r.Writer).Encode(v) 191 | if err != nil { 192 | panic(err) 193 | } 194 | } 195 | } 196 | 197 | func unsafePrinter(w io.Writer, b []byte) { 198 | w.Write(b) 199 | } 200 | 201 | // SafeWriter is a function that writes bytes directly to the render output, without going through Jet's auto-escaping phase. 202 | // Use/implement this if content should be escaped differently or not at all (see raw/unsafe builtins). 203 | type SafeWriter func(io.Writer, []byte) 204 | 205 | var stringType = reflect.TypeOf("") 206 | 207 | var newMap = Func(func(a Arguments) reflect.Value { 208 | if a.NumOfArguments()%2 > 0 { 209 | panic("map(): incomplete key-value pair (even number of arguments required)") 210 | } 211 | 212 | m := reflect.ValueOf(make(map[string]interface{}, a.NumOfArguments()/2)) 213 | 214 | for i := 0; i < a.NumOfArguments(); i += 2 { 215 | key := a.Get(i) 216 | if !key.IsValid() { 217 | a.Panicf("map(): key argument at position %d is not a valid value!", i) 218 | } 219 | if !key.Type().ConvertibleTo(stringType) { 220 | a.Panicf("map(): can't use %+v as string key: %s is not convertible to string", key, key.Type()) 221 | } 222 | key = key.Convert(stringType) 223 | m.SetMapIndex(a.Get(i), a.Get(i+1)) 224 | } 225 | 226 | return m 227 | }) 228 | 229 | var newSlice = Func(func(a Arguments) reflect.Value { 230 | arr := make([]interface{}, a.NumOfArguments()) 231 | for i := 0; i < a.NumOfArguments(); i++ { 232 | arr[i] = a.Get(i).Interface() 233 | } 234 | return reflect.ValueOf(arr) 235 | }) 236 | -------------------------------------------------------------------------------- /docs/builtins.md: -------------------------------------------------------------------------------- 1 | # Built-ins 2 | 3 | - [Functions](#functions) 4 | - [From Go](#from-go) 5 | - [len](#len) 6 | - [isset](#isset) 7 | - [exec](#exec) 8 | - [ints](#ints) 9 | - [dump](#dump) 10 | - [SafeWriter](#safewriter) 11 | - [safeHtml](#safehtml) 12 | - [safeJs](#safejs) 13 | - [raw/unsafe](#rawunsafe) 14 | - [Renderer](#renderer) 15 | - [writeJson](#writejson) 16 | 17 | ## Functions 18 | 19 | ### From Go 20 | 21 | The following functions simply expose functions from Go's standard library for convenience: 22 | 23 | - `lower`: exposes Go's [strings.ToLower](https://golang.org/pkg/strings/#ToLower) 24 | - `upper`: exposes Go's [strings.ToUpper](https://golang.org/pkg/strings/#ToUpper) 25 | - `hasPrefix`: exposes Go's [strings.HasPrefix](https://golang.org/pkg/strings/#HasPrefix) 26 | - `hasSuffix`: exposes Go's [strings.HasSuffix](https://golang.org/pkg/strings/#HasSuffix) 27 | - `repeat`: exposes Go's [strings.Repeat](https://golang.org/pkg/strings/#Repeat) 28 | - `replace`: exposes Go's [strings.Replace](https://golang.org/pkg/strings/#Replace) 29 | - `split`: exposes Go's [strings.Split](https://golang.org/pkg/strings/#Split) 30 | - `trimSpace`: exposes Go's [strings.TrimSpace](https://golang.org/pkg/strings/#TrimSpace) 31 | - `html`: exposes Go's [html.EscapeString](https://golang.org/pkg/html/#EscapeString) 32 | - `url`: exposes Go's [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape) 33 | - `json`: exposes Go's [json.Marshal](https://golang.org/pkg/encoding/json/#Marshal) 34 | 35 | ### len 36 | 37 | `len()` takes one argument and returns the length of a string, array, slice or map, the number of fields in a struct, or the buffer size of a channel, depending on the argument's type. (Think of it like Go's `len()` function.) 38 | 39 | It panics if you pass a value of any type other than string, array, slice, map, struct or channel. 40 | 41 | `len()` indirects through arbitrary layers of pointer and interface types before checking for a valid type. 42 | 43 | ### isset 44 | 45 | `isset()` takes an arbitrary number of index, field, chain or identifier expressions and returns true if all expressions evaluate to non-nil values. It panics only when an unexpected expression type is passed in. 46 | 47 | ### exec 48 | 49 | `exec()` takes a template path and optionally a value to use as context and executes the template with the current or specified context. It returns the last value returned using the `return` statement, or nil if no `return` statement was executed. 50 | 51 | ### ints 52 | 53 | `ints()` takes two integers as lower and upper limit and returns a Ranger producing all the integers between them, including the lower and excluding the upper limit. It panics when the arguments can't be converted to integers or when the upper limit is not strictly greater than the lower limit. 54 | 55 | ### dump 56 | 57 | `dump` is meant to aid in template development, and can be used to print out variables, blocks, context, and globals that are available to the template. 58 | The function can be used in three forms: 59 | 60 | `dump()` used without parameters will print out context, variables, globals, and blocks (in this order) in the current scope, without accessing any parent. 61 | 62 | `dump(levels)` - where `levels` is an **integer** - is the same as `dump()`, but will additionally recurse over context parents to the maximum depth of `levels`. 63 | For example, `dump(1)` will additionaly print out all variables accessible in the direct parent of the current context. 64 | 65 | `dump("name1", "name2", ...)` will search for the variable and/or block with the given name(s) in any scope (current and all parents) of the current runtime. 66 | 67 | ## SafeWriter 68 | 69 | Jet includes a [`SafeWriter`](https://pkg.go.dev/github.com/CloudyKit/jet/v5?tab=doc#SafeWriter) function type for writing directly to the render output stream. This can be used to circumvent Jet's default HTML escaping. Jet has a few such functions built-in. 70 | 71 | ### safeHtml 72 | 73 | `safeHtml` is an alias for Go's [template.HTMLEscape](https://golang.org/pkg/text/template/#HTMLEscape) (converted to the `SafeWriter` type). This is the same escape function that's also applied to the evalutation result of action nodes by default. It escapes everything that could be interpreted as HTML. 74 | 75 | ### safeJs 76 | 77 | `safeJs` is an alias for Go's [template.JSEscape](https://golang.org/pkg/text/template/#JSEscape). It escapes data to be safe to use in a Javascript context. 78 | 79 | ### raw/unsafe 80 | 81 | `raw` (alias `unsafe`) is a writer that escapes nothing at all, allowing you to circumvent Jet's default HTML escaping. Use with caution! 82 | 83 | ## Renderer 84 | 85 | Jet exports a [`Renderer`](https://pkg.go.dev/github.com/CloudyKit/jet/v5?tab=doc#Renderer) interface (and [`RendererFunc`](https://pkg.go.dev/github.com/CloudyKit/jet/v5?tab=doc#RendererFunc) type which implements the interface). When an action evaluates to a value implementinng this interface, it will not be rendered using [fastprinter](https://github.com/CloudyKit/fastprinter), but by calling its Render() function instead. 86 | 87 | #### writeJson 88 | 89 | `writeJson` renders the JSON encoding of whatever you pass in to the output, escaping only "<", ">", and "&" (just like the `json` function). 90 | -------------------------------------------------------------------------------- /docs/changes.md: -------------------------------------------------------------------------------- 1 | # Breaking Changes 2 | 3 | ## v6 4 | 5 | When udpating from version 5 to version 6, there are breaking changes to the Go API: 6 | 7 | - Set's LoadTemplate() method was removed 8 | 9 | LoadTemplate() (which used to parse and cache a template while bypassing the Set's Loader) is removed in favor of the [new in-memory Loader](https://godoc.org/github.com/CloudyKit/jet#InMemLoader) where you can add templates on-the-fly (which is also used for tests without template files). Using it together with a file Loader via a MultiLoader restores the previous functionality: having template files accessed via the Loader and purely in-memory templates on top of those. [#182](https://github.com/CloudyKit/jet/pull/182) 10 | 11 | - Loader interface changed 12 | 13 | A Loader's Exists() method does not return the path to the template if it was found. Jet doesn't really care about what path the Loader implementation uses to locate the template. Jet expects the Loader to guarantee that the path it tried in Exists() to also work in calls to Open(), when the Exists() call returned true. [#183](https://github.com/CloudyKit/jet/pull/183) 14 | 15 | - a new Cache interface was introduced 16 | 17 | Previously, it was impossible to control if and how long a Set caches templates it parsed after fetching them via the Loader. Now, you can pass a custom Cache implementation to have complete control over caching behavior, for example to invalidate a cached template and making a Set re-fetch it via the Loader and re-parse it. [#183](https://github.com/CloudyKit/jet/pull/183) 18 | 19 | - new NewSet() with option functions 20 | 21 | The different functions used to create a Set (NewSet, NewSetLoader, NewHTMLSet, NewHTMLSetLoader()) have been removed in favor of a single NewSet() function that requires only a loader and accepts any number of configuration options in the form of option functions. When not passing any options, NewSet() will now use the HTML safe writer by default. 22 | 23 | - SetDevelopmentMode(), Delims(), SetExtensions() converted to option functions 24 | 25 | The new InDevelopmentMode(), WithDelims() and WithTemplateNameExtensions() option functions replace the previous functions. This means you can't change these settings after the Set is created, which was very likely not a good idea anyway. 26 | 27 | If you toggle development mode after Set creation, you can now use a custom Cache to configure cache-use on the fly. Since this is all the development mode does anyway, the InDevelopmentMode() option might be removed in a future major version of Jet. 28 | 29 | There are no breaking changes to the template language. 30 | 31 | ## v5 32 | 33 | When updating from version 4 to version 5, there is a breaking change: 34 | 35 | - `_` became a reserved symbol 36 | 37 | Version 5 uses `_` for two new features: it adds Go-like discard syntax in assignments (assigning anything to `_` will make jet skip the assignment) and to denote the [argument slot for the piped value](./syntax.md#piped-argument-slot). When assigning to `_`, Jet will still always evaluate the corresponding right-hand side of the assignment statement, i.e. you can use `_` to call a function but throw away its return value. 38 | 39 | When you assign (and/or use) a variable called `_` in your code, you will have to rename this variable. 40 | 41 | ## v4 42 | 43 | When updating from version 3 to version 4, there are a few breaking changes: 44 | 45 | - one-variable assignment in `range` 46 | 47 | `range x := someSlice` would set `x` to the value of the element in v3. In v4, `x` will be the index of the element. (Ranging over a channel didn't change.) 48 | See https://github.com/CloudyKit/jet/issues/158. 49 | 50 | - Runtime.Set() 51 | 52 | In v3, Set() would initialise a new variable in the top scope if no variable with that name existed. In v4, Set() will return an error when trying to set a variable that doesn't exist. Let() now always sets a variable in the current scope (possibly shadowing an existing one in a parent scope). SetOrLet() will try to change the value of an existing variable and only initialize a new variable in the current scope, if the variable doesn't exist. LetGlobal() is like Let() but always acts on the top scope. 53 | 54 | - new keywords `return`, `try`, `catch` and builtins `exec`, `ints`, `slice`, `array` 55 | 56 | `return`, `try`, `catch`, `exec`, `ints`, `slice` and `array` are now keywords or predefined identifiers. If you previously used `return`, `try` or `catch`, you will have to rename your variables. `exec`, `ints`, `slice` and `array` can technically be overwritten, but you should make sure not to name your things those words regardless. 57 | 58 | - OSFileSystemLoader only handles a single directory 59 | 60 | Use loaders.Multi to load templates from multiple directories. See https://github.com/CloudyKit/jet/issues/128. 61 | 62 | - relative paths 63 | 64 | Relative paths to templates are now handled correctly. See https://github.com/CloudyKit/jet/issues/127. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - [Breaking Changes](./changes.md) 4 | - [Syntax Reference](./syntax.md) 5 | - [Built-ins](./builtins.md) -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax Reference 2 | 3 | - [Delimiters](#delimiters) 4 | - [Whitespace Trimming](#whitespace-trimming) 5 | - [Comments](#comments) 6 | - [Variables](#variables) 7 | - [Initialization](#initialization) 8 | - [Assignment](#assignment) 9 | - [Expressions](#expressions) 10 | - [Identifiers](#identifiers) 11 | - [Indexing](#indexing) 12 | - [String](#string) 13 | - [Slice / Array](#slice--array) 14 | - [Map](#map) 15 | - [Struct](#struct) 16 | - [Field access](#field-access) 17 | - [Map](#map-1) 18 | - [Struct](#struct-1) 19 | - [Slicing](#slicing) 20 | - [Arithmetic](#arithmetic) 21 | - [String concatenation](#string-concatenation) 22 | - [Logical operators](#logical-operators) 23 | - [Ternary operator](#ternary-operator) 24 | - [Method calls](#method-calls) 25 | - [Function calls](#function-calls) 26 | - [Prefix syntax](#prefix-syntax) 27 | - [Pipelining](#pipelining) 28 | - [Piped argument slot](#piped-argument-slot) 29 | - [Control Structures](#control-structures) 30 | - [if](#if) 31 | - [if / else](#if--else) 32 | - [if / else if](#if--else-if) 33 | - [if / else if / else](#if--else-if--else) 34 | - [range](#range) 35 | - [Slices / Arrays](#slices--arrays) 36 | - [Maps](#maps) 37 | - [Channels](#channels) 38 | - [Custom](#custom-ranger) 39 | - [else](#else) 40 | - [try](#try) 41 | - [try / catch](#try--catch) 42 | - [Templates](#templates) 43 | - [include](#include) 44 | - [return](#return) 45 | - [Blocks](#blocks) 46 | - [block](#block) 47 | - [yield](#yield) 48 | - [content](#content) 49 | - [Recursion](#recursion) 50 | - [extends](#extends) 51 | - [import](#import) 52 | 53 | ## Delimiters 54 | 55 | Template delimiters are `{{` and `}}`. Delimiters can use `.` to output the execution context, and they can also contain many different literals, expressions, and declarations. 56 | 57 | hello {{ . }} 58 | 59 | Jet can also be configured to use alternative delimiters, such as `[[` and `]]`. 60 | 61 | hello [[ . ]] 62 | 63 | ### Whitespace Trimming 64 | 65 | By default, all text outside of template delimiters is copied verbatim when the template is parsed and executed, including whitespace. 66 | 67 | foo {{ "bar" }} baz 68 | 69 | To aid in formatting template source code, Jet can be instructed to trim whitepsace preceding and following delimiters using the `{{- ` and ` -}}` syntax. (This could be `[[- ` and ` -]]` with alternate delimiters, for example.) Note the space character adjacent to the dash which must be present. 70 | 71 | foo {{- "bar" -}} baz 72 | 73 | For this trimming, whitespace is defined as: spaces, horizontal tabs, carriage returns, and newlines. 74 | 75 | ### Comments 76 | 77 | Comments begin with `{*` and end with `*}` and will simply be dropped during template parsing. 78 | 79 | {* this is a comment *} 80 | 81 | Comments can span multiple lines: 82 | 83 | {* 84 | none of this will be executed: 85 | {{ asd }} 86 | {{ include "./foo.jet" }} 87 | *} 88 | 89 | ## Variables 90 | 91 | ### Initialization 92 | 93 | Variables have to be initialised before they can be used: 94 | 95 | {{ foo := "bar" }} 96 | 97 | ### Assignment 98 | 99 | Initialised variables can be assigned a new value: 100 | 101 | {{ foo = "asd" }} 102 | 103 | Variables initialised inside a template have no fixed type, so this is valid, too: 104 | 105 | {{ foo = 4711 }} 106 | 107 | Assigning anything to `_` tells Jet to evalute the right side, but skip the actual assignment to a new or existing identifier. This is useful to call a function but discard its return value: 108 | 109 | {{ _ := stillRuns() }} 110 | {{ _ = stillRuns() }} 111 | 112 | Since no actual assigning takes place, both of the above are equivalent: `stillRuns` is executed, but the return value will neither be stored in a variable, nor will it be rendered (unlike `{{ stillRuns() }}`, which would render the return value to the output). 113 | 114 | ## Expressions 115 | 116 | ### Identifiers 117 | 118 | Function and variable names are identifiers. Identifiers simply evaluate to the value stored for them in a variable scope, the globals, or the default variables. For example, the following are identifiers that resolve to built-in functions: 119 | 120 | - `len` 121 | - `isset` 122 | - `split` 123 | 124 | After `{{ foo := "foo" }}`, the `foo` in `{{ len(foo) }}` is an identifier expression and resolved to the string "foo". 125 | 126 | ### Indexing 127 | 128 | Indexing expressions use `[]` syntax and evaluate to a byte in a string, an element in a slice or array, a value in a map, or a field of a struct. 129 | 130 | #### String 131 | 132 | {{ s := "helloworld" }} 133 | {{ s[1] }} 134 | 135 | #### Slice / Array 136 | 137 | {{ s := slice("foo", "bar", "asd") }} 138 | {{ s[0] }} 139 | {{ i := 2 }} 140 | {{ s[i] }} 141 | 142 | #### Map 143 | 144 | {{ m := map("foo", 123, "bar", 456) }} 145 | {{ m["foo"] }} 146 | {{ bar := "bar" }} 147 | {{ m[bar] }} 148 | 149 | #### Struct 150 | 151 | Assuming `user` is a Go struct value with a string field "Name": 152 | 153 | {{ user["Name"] }} 154 | 155 | ### Field access 156 | 157 | Field access expressions use dot notation (`foo.bar`) and can be used with maps or structs. When the identifier in front of the `.` is omitted, the field is looked up in the current context (which will fail if the context is not a map or struct). 158 | 159 | #### Map 160 | 161 | {{ m := map("foo", 123, "bar", 456) }} 162 | {{ m.foo }} 163 | {{ s := slice(m, map("foo", 4711)) }} 164 | {{ range s }} 165 | {{ .foo }} 166 | {{ end }} 167 | 168 | #### Struct 169 | 170 | Assuming `user` is a Go struct value with a string field "Name": 171 | 172 | {{ user.Name }} 173 | 174 | Assuming `users` is a slice of Go structs, o: 175 | 176 | {{ range users }} 177 | {{ .Name }} 178 | {{ end }} 179 | 180 | ### Slicing 181 | 182 | You may re-slice a slice or array using the Go-like [start:end] syntax. The element at the `start` index will be included, the one at the `end` index will be excluded. 183 | 184 | {{ s := slice(6, 7, 8, 9, 10, 11) }} 185 | {{ sevenEightNine := s[1:4] }} 186 | 187 | ### Arithmetic 188 | 189 | Basic arithmetic operators are supported: `+`, `-`, `*`, `/`, `%` 190 | 191 | {{ 1 + 2 * 3 - 4 }} 192 | {{ (1 + 2) * 3 - 4.1 }} 193 | 194 | ### String concatenation 195 | 196 | {{ "HELLO" + " " + "WORLD!" }} 197 | 198 | #### Logical operators 199 | 200 | The following operators are supported: 201 | 202 | - `&&`: and 203 | - `||`: or 204 | - `!`: not 205 | - `==`: equal 206 | - `!=`: not equal 207 | - `>`: greater than 208 | - `>=`: greater than or equal (= not less than) 209 | - `<`: less than 210 | - `<=`: less than or equal (= not greater than) 211 | 212 | Examples: 213 | 214 | {{ item == true || !item2 && item3 != "test" }} 215 | 216 | {{ item >= 12.5 || item < 6 }} 217 | 218 | Logical expressions always evaluate to either `true` or `false`. 219 | 220 | ### Ternary operator 221 | 222 | ` x ? y : z` evaluates to `y` if `x` is truthy or `z` otherwise. 223 | 224 | {{ .HasTitle ? .Title : "Title not set" }} 225 | 226 | ### Method calls 227 | 228 | You can call exported methods of Go types: 229 | 230 | {{ user.Rename("Peter") }} 231 | {{ range users }} 232 | {{ .FullName() }} 233 | {{ end }} 234 | 235 | ### Function calls 236 | 237 | Function calls can be written using familiar C-like syntax: 238 | 239 | {{ len(s) }} 240 | {{ isset(foo, bar) }} 241 | 242 | #### Prefix syntax 243 | 244 | Function calls can also be written using a colon instead of parentheses: 245 | 246 | {{ len: s }} 247 | {{ isset: foo, bar }} 248 | 249 | Note that function calls using this syntax can't be nested! This is valid: `{{ len: slice("asd", "foo") }}`, but this isn't: `{{ len: slice: "asd", "foo" }}` 250 | 251 | #### Pipelining 252 | 253 | Pipelining works by "piping" a value into a function as its first argument: 254 | 255 | {{ "123" | len}} 256 | {{ "FOO" | lower | len }} 257 | 258 | Pipelines are evaluated left-to-right. This chaining syntax may be easier to read than deeply nested calls: 259 | 260 | {{ "123" | lower | upper | len }} 261 | 262 | is equivalent to 263 | 264 | {{ len(upper(lower("123"))) }} 265 | 266 | Inside a pipeline, functions can be enriched with additional parameters: 267 | 268 | {{ "hello" | repeat: 2 | len }} 269 | {{ "hello" | repeat(2) | len }} 270 | 271 | Both of the above are equivalent to this: 272 | 273 | {{ len(repeat("hello", 2)) }} 274 | 275 | Please note that the raw, unsafe, safeHtml or safeJs built-in escapers (or custom safe writers) need to be the last command evaluated in an action node. This means they have to come last when used in a pipeline. 276 | 277 | {{ "hello" | upper | raw }} 278 | {{ raw: "hello" }} 279 | {{ raw: "hello" | upper }} 280 | 281 | #### Piped argument slot 282 | 283 | When pipelining, it can be desirable to use the piped value in a different slot in the function call than the first. To tell Jet where to inject the piped value, use `_`: 284 | 285 | {{ 2 | repeat("foo", _) }} 286 | {{ 2 | repeat("foo", _) | repeat(_, 3) }} 287 | 288 | All of the following produce the same output as the second line in the example above: 289 | 290 | {{ 2 | repeat("foo", _) | repeat(3) }} 291 | {{ 2 | repeat: "foo", _ | repeat: 3 }} 292 | {{ 2 | repeat: "foo", _ | repeat: _, 3 }} 293 | 294 | This feature is inspired by [function capturing](https://gleam.run/tour/functions.html#function-capturing) in Gleam. 295 | 296 | ## Control Structures 297 | 298 | ### if 299 | 300 | You can branch inside templates depending on a condition using `if`: 301 | 302 | {{ if foo == "asd" }} 303 | foo is 'asd'! 304 | {{ end }} 305 | 306 | #### if / else 307 | 308 | You may provide an `else` block when using `if`: 309 | 310 | {{ if foo == "asd" }} 311 | foo is 'asd'! 312 | {{ else }} 313 | foo is something else! 314 | {{ end }} 315 | 316 | #### if / else if 317 | 318 | You can test for another condition using `else if`: 319 | 320 | {{ if foo == "asd" }} 321 | foo is 'asd'! 322 | {{ else if foo == 4711 }} 323 | foo is 4711! 324 | {{ end }} 325 | 326 | This is exactly the same as this code: 327 | 328 | {{ if foo == "asd" }} 329 | foo is 'asd'! 330 | {{ else }} 331 | {{ if foo == 4711 }} 332 | foo is 4711! 333 | {{ end }} 334 | {{ end }} 335 | 336 | 337 | #### if / else if / else 338 | 339 | `if / else if / else` works, too, of course: 340 | 341 | {{ if foo == "asd" }} 342 | foo is 'asd'! 343 | {{ else if foo == 4711 }} 344 | foo is 4711! 345 | {{ else }} 346 | foo is something else! 347 | {{ end }} 348 | 349 | and will do exactly the same as this: 350 | 351 | {{ if foo == "asd" }} 352 | foo is 'asd'! 353 | {{ else }} 354 | {{ if foo == 4711 }} 355 | foo is 4711! 356 | {{ else }} 357 | foo is something else! 358 | {{ end }} 359 | {{ end }} 360 | 361 | ### range 362 | 363 | Use `range` to iterate over data, just like you would in Go, or how you would use a `foreach` loop in other programming languages. Inside the `range`, the context (`.`) is set to the current iteration's value: 364 | 365 | {{ s := slice("foo", "bar", "asd") }} 366 | {{ range s }} 367 | {{.}} 368 | {{ end }} 369 | 370 | Jet provides built-in rangers for Go slices, arrays, maps, and channels. You can add your own by implementing the Ranger interface. 371 | 372 | #### Slices / Arrays 373 | 374 | When iterating over a slice or array, Jet can give you the current iteration index: 375 | 376 | {{ range i := s }} 377 | {{i}}: {{.}} 378 | {{ end }} 379 | 380 | If you want, you can have Jet assign the iteration value to another value: 381 | 382 | {{ range i, v := s }} 383 | {{i}}: {{v}} 384 | {{ end }} 385 | 386 | The iteration value will then not be used as context (`.`); instead, the parent context remains available. 387 | 388 | #### Maps 389 | 390 | When iterating over a map, Jet can give you the key current iteration index: 391 | 392 | {{ m := map("foo", "bar", "asd", 123)}} 393 | {{ range k := m }} 394 | {{k}}: {{.}} 395 | {{ end }} 396 | 397 | Just like with slices, you can have Jet assign the iteration value to another value: 398 | 399 | {{ range k, v := m }} 400 | {{k}}: {{v}} 401 | {{ end }} 402 | 403 | The iteration value will then not be used as context (`.`); instead, the parent context remains available. 404 | 405 | #### Channels 406 | 407 | When iterating over a channel, you can have Jet assign the iteration value to another value in order to keep the parent context, similar to the two-variables syntax for slices and maps: 408 | 409 | {{ range v := c }} 410 | {{v}} 411 | {{ end }} 412 | 413 | It's an error to use channels together with the two-variable syntax. 414 | 415 | #### Custom Ranger 416 | 417 | Any value that implements the 418 | [Ranger](https://pkg.go.dev/github.com/CloudyKit/jet/v6#Ranger) interface can be 419 | used for ranging over values. Look in the package docs for an example. 420 | 421 | #### else 422 | 423 | `range` statements can have an `else` block which is executed if there are non values to range over (as signalled by the Ranger). For example, it will run when iterating an empty slice, array or map or a closed channel: 424 | 425 | {{ range searchResults }} 426 | {{.}} 427 | {{ else }} 428 | No results found :( 429 | {{ end }} 430 | 431 | ### try 432 | 433 | If you want to attempt rendering something, but don't want Jet to crash when something goes wrong, you can use `try`: 434 | 435 | {{ try }} 436 | we're not sure if we already initialised foo, 437 | so the next line might fail... 438 | {{ foo }} 439 | {{ end }} 440 | 441 | You can do anything you want inside a `try` block, even yield blocks or include other templates. 442 | 443 | All render output generated inside the `try` block is buffered and only included in the surrounding output after execution of the entire block completed successfully. Any runtime error means no content from inside `try` is kept. 444 | 445 | ### try / catch 446 | 447 | In case of an error inside the `try` block, you can have Jet evaluate a `catch` block: 448 | 449 | {{ try }} 450 | we're not sure if we already initialised foo, 451 | so the next line might fail... 452 | {{ foo }} 453 | {{ catch }} 454 | foo was not initialised, this is fallback content 455 | {{ end }} 456 | 457 | Errors occuring inside the `catch` block are not caught and will cause execution to abort. 458 | 459 | You can also have the error that occured assigned to a variable inside the `catch` block to log it or otherwise handle it. Since it's a Go error value, you have to call `.Error()` on it to get the error message as a string. 460 | 461 | {{ try }} 462 | we're not sure if we already initialised foo, 463 | so the next line might fail... 464 | {{ foo }} 465 | {{ catch err }} 466 | {{ log(err.Error()) }} 467 | uh oh, something went wrong: {{ err.Error() }} 468 | {{ end }} 469 | 470 | `err` will not be available outside the `catch` block. 471 | 472 | ## Templates 473 | 474 | ### include 475 | 476 | Including a template is similar to using partials in other template languages. All local and global variables are available to you in the included template. You can pass a context by specifying it as the last argument in the `include` statement. If you don't pass a context, the current context will be kept. 477 | 478 | 479 |
480 | {{ .["name"] }}: {{ .["email"] }} 481 |
482 | 483 | 484 | {{ range users }} 485 | {{ include "./user.jet" }} 486 | {{ end }} 487 | 488 | Executing `index.jet` with 489 | 490 | {{ users := map( 491 | "4243", map("name", "Peter", "email", "peter@aol.com"), 492 | "4534", map("name", "Bob", "email", "bob@yahoo.com") 493 | ) }} 494 | 495 | gives you: 496 | 497 |
498 | Peter: peter@aol.com 499 |
500 |
501 | Bob: bob@yahoo.com 502 |
503 | 504 | ### return 505 | 506 | Templates can set a value as their return value using `return`. This is only useful when the template was executed using the `exec()` built-in function, which will make the return value of a template available in another template. 507 | 508 | `return` will **not** stop execution of the current block or template! 509 | 510 | 511 | {{ f := "f" }} 512 | {{ o := "o" }} 513 | {{ return f+o+o }} 514 | 515 | 516 | {{ foo := exec("./foo.jet") }} 517 | Hello, {{ foo }}! 518 | 519 | The output will simply be: 520 | 521 | Hello, foo! 522 | 523 | ## Blocks 524 | 525 | You can think of blocks as partials or pieces of a template that you can invoke by name. 526 | 527 | ### block 528 | 529 | To define a block, use `block`: 530 | 531 | {{block copyright()}} 532 |
© ACME, Inc. 2020
533 | {{end}} 534 | 535 | Defining a block in a template that's being executed will also invoke it immediately. To avoid this, use `import` or `extends`. Blocks can't be named "content", "yield", or other Jet keywords. 536 | 537 | A block definition accepts a comma-separated list of argument names, with optional defaults: 538 | 539 | {{ block inputField(type="text", label, id, value="", required=false) }} 540 |
541 | 542 | 543 |
544 | {{ end }} 545 | 546 | ### yield 547 | 548 | To invoke a previously defined block, use `yield`: 549 | 550 |
551 | {{yield copyright()}} 552 |
553 | 554 | {{yield inputField(id="firstname", label="First name", required=true)}} 555 | 556 | The sequence of parameters is irrelevant, and parameters without a default value must be passed when yielding a block. 557 | 558 | You can pass something to be used as context, or the current context will be passed. Given 559 | 560 | {{block buff()}} 561 | {{.}} 562 | {{end}} 563 | 564 | the following invocation 565 | 566 | {{yield buff() "Batman"}} 567 | 568 | will produce 569 | 570 | Batman 571 | 572 | ### content 573 | 574 | When defining a block, use the special `{{ yield content }}` statement to designate where any inner content should be rendered. Then, when you invoke the block with yield, use the keyword content at the end of the `yield`. For example: 575 | 576 | {{ block link(target) }} 577 | {{ yield content }} 578 | {{ end }} 579 | 580 | [...] 581 | 582 | {{ yield link(target="https://www.example.com") content }} 583 | Example Inc. 584 | {{ end }} 585 | 586 | The output will be 587 | 588 | Example Inc. 589 | 590 | The invocating `yield` (`{{ yield link(target="https://www.example.com") content }}`) will store the content (together with the current variable scope) and the `{{ yield content }}` part will restore the variable scope and execute the content. When you pass a context during the `yield` block invocation, it will be used when executing the content as well. 591 | 592 | Since defining a block will also invoke it, you can also define some content immediately as part of the `block` definition: 593 | 594 | {{ name := "Sarah" }} 595 | {{ block header() }} 596 |
597 | {{ yield content }} 598 |
599 | {{ content }} 600 |

Hey {{ name }}!

601 | {{ end }} 602 | 603 | This will render something like the following at the position where the block is defined: 604 | 605 |
606 |

Hey Sarah!

607 |
608 | 609 | ### Recursion 610 | 611 | You can yield a block inside its own definition: 612 | 613 | {{ block menu() }} 614 | 619 | {{ end }} 620 | 621 | ### extends 622 | 623 | A template can extend another template using an `extends` statement followed by a template path at the very top: 624 | 625 | 626 | {{extends "./layout.jet"}} 627 | 628 | In an extending template, content outside of a block definition will be discarded: 629 | 630 | 631 | {{extends "./layout.jet"}} 632 | {{block body()}} 633 |
634 | This content can be yielded anywhere. 635 |
636 | {{end}} 637 | This content will never be rendered. 638 | 639 | The extended template then has to have yield slots to render your blocks into: 640 | 641 | 642 | 643 | 644 | 645 | {{yield body()}} 646 | 647 | 648 | 649 | The final result will be: 650 | 651 | 652 | 653 | 654 |
655 | This content can be yielded anywhere. 656 |
657 | 658 | 659 | 660 | Every template can only extend one other template, and the `extends` statement has to be at the very top of the file (even above `import` statements). 661 | 662 | Since the extending template isn't actually executed (the extended template is), the blocks defined in it don't run until you `yield` them explicitely. 663 | 664 | ### import 665 | 666 | A template's defined blocks can be imported into another template using the `import` statement: 667 | 668 | 669 | {{ block body() }} 670 |
671 | This content can be yielded anywhere. 672 |
673 | {{ end }} 674 | 675 | 676 | {{ import "./my_blocks.jet" }} 677 | 678 | 679 | 680 | {{ yield body() }} 681 | 682 | 683 | 684 | Executing `index.jet` will produce: 685 | 686 | 687 | 688 | 689 |
690 | This content can be yielded anywhere. 691 |
692 | 693 | 694 | 695 | `import` makes all the blocks from the imported template available in the importing template. There is no way to only import (a) specific block(s). 696 | 697 | Since the imported template isn't actually executed, the blocks defined in it don't run until you `yield` them explicitely. 698 | -------------------------------------------------------------------------------- /dump.go: -------------------------------------------------------------------------------- 1 | package jet 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | ) 9 | 10 | // dumpAll returns 11 | // - everything in Runtime.context 12 | // - everything in Runtime.variables 13 | // - everything in Runtime.set.globals 14 | // - everything in Runtime.blocks 15 | func dumpAll(a Arguments, depth int) reflect.Value { 16 | var b bytes.Buffer 17 | var vars VarMap 18 | 19 | ctx := a.runtime.context 20 | fmt.Fprintln(&b, "Context:") 21 | fmt.Fprintf(&b, "\t%s %#v\n", ctx.Type(), ctx) 22 | 23 | dumpScopeVars(&b, a.runtime.scope, 0) 24 | dumpScopeVarsToDepth(&b, a.runtime.parent, depth) 25 | 26 | vars = a.runtime.set.globals 27 | for i, name := range vars.SortedKeys() { 28 | if i == 0 { 29 | fmt.Fprintln(&b, "Globals:") 30 | } 31 | val := vars[name] 32 | fmt.Fprintf(&b, "\t%s:=%#v // %s\n", name, val, val.Type()) 33 | } 34 | 35 | blockKeys := a.runtime.scope.sortedBlocks() 36 | fmt.Fprintln(&b, "Blocks:") 37 | for _, k := range blockKeys { 38 | block := a.runtime.blocks[k] 39 | dumpBlock(&b, block) 40 | } 41 | 42 | return reflect.ValueOf(b.String()) 43 | } 44 | 45 | // dumpScopeVarsToDepth prints all variables in the scope, and all parent scopes, 46 | // to the limit of maxDepth. 47 | func dumpScopeVarsToDepth(w io.Writer, scope *scope, maxDepth int) { 48 | for i := 1; i <= maxDepth; i++ { 49 | if scope == nil { 50 | break // do not panic if something bad happens 51 | } 52 | dumpScopeVars(w, scope, i) 53 | scope = scope.parent 54 | } 55 | } 56 | 57 | // dumpScopeVars prints all variables in the scope. 58 | func dumpScopeVars(w io.Writer, scope *scope, lvl int) { 59 | if scope == nil { 60 | return // do not panic if something bad happens 61 | } 62 | if lvl == 0 { 63 | fmt.Fprint(w, "Variables in current scope:\n") 64 | } else { 65 | fmt.Fprintf(w, "Variables in scope %d level(s) up:\n", lvl) 66 | } 67 | vars := scope.variables 68 | for _, k := range vars.SortedKeys() { 69 | fmt.Fprintf(w, "\t%s=%#v\n", k, vars[k]) 70 | } 71 | } 72 | 73 | // dumpIdentified accepts a runtime and slice of names. 74 | // Then, it prints all variables and blocks in the runtime, with names equal to one of the names 75 | // in the slice. 76 | func dumpIdentified(rnt *Runtime, ids []string) reflect.Value { 77 | var b bytes.Buffer 78 | for _, id := range ids { 79 | dumpFindVar(&b, rnt, id) 80 | dumpFindBlock(&b, rnt, id) 81 | 82 | } 83 | return reflect.ValueOf(b.String()) 84 | } 85 | 86 | // dumpFindBlock finds the block by name, prints the header of the block, and name of the template in which it was defined. 87 | func dumpFindBlock(w io.Writer, rnt *Runtime, name string) { 88 | if block, ok := rnt.scope.blocks[name]; ok { 89 | dumpBlock(w, block) 90 | } 91 | } 92 | 93 | // dumpBlock prints header of the block, and template in which the block was first defined. 94 | func dumpBlock(w io.Writer, block *BlockNode) { 95 | if block == nil { 96 | return 97 | } 98 | fmt.Fprintf(w, "\tblock %s(%s), from %s\n", block.Name, block.Parameters.String(), block.TemplatePath) 99 | } 100 | 101 | // dumpFindBlock finds the variable by name, and prints the variable, if it is in the runtime. 102 | func dumpFindVar(w io.Writer, rnt *Runtime, name string) { 103 | val, err := rnt.resolve(name) 104 | if err != nil { 105 | return 106 | } 107 | fmt.Fprintf(w, "\t%s:=%#v // %s\n", name, val, val.Type()) 108 | } 109 | -------------------------------------------------------------------------------- /dump_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "bytes" 19 | "reflect" 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestDump(t *testing.T) { 25 | var b bytes.Buffer // writer for the template 26 | tmplt, err := parseSet.GetTemplate("devdump.jet") // the testing template containing dump function 27 | if err != nil { 28 | t.Log(err) 29 | t.FailNow() 30 | } 31 | 32 | // execute template with dummy inputs 33 | // MAP 34 | vars := make(VarMap) 35 | aMap := make(map[string]interface{}) 36 | aMap["aMap-10"] = 10 // only one member, because map is unsorted; test could fail for no apparent reason. 37 | vars.Set("inputMap", aMap) 38 | // SLICE 39 | aSlice := []string{"sliceMember1", "sliceMember2"} 40 | vars.Set("aSlice", aSlice) 41 | 42 | // prepare dummy context 43 | ctx := struct { 44 | Name string 45 | Surname string 46 | }{Name: "John", Surname: "Doe"} 47 | 48 | // execute template 49 | err = tmplt.Execute(&b, vars, ctx) 50 | if err != nil { 51 | t.Log(err) 52 | t.FailNow() 53 | } 54 | 55 | // normalize EOL convention and 56 | // split outcome to two parts; this is necessary, because the original code 57 | // was developed on windows (SORRY !!!!) 58 | aux := strings.ReplaceAll(b.String(), "\r\n", "\n") 59 | rslt := strings.Split(aux, "===\n") 60 | if len(rslt) != 2 { 61 | t.Log("expected to get two parts, did you include separator in the template?") 62 | t.FailNow() 63 | } 64 | //t.Log(rslt[0]) 65 | // compare what we got with what we wanted 66 | got := strings.Split(rslt[0], "\n") 67 | want := strings.Split(rslt[1], "\n") 68 | if !reflect.DeepEqual(got, want) { 69 | t.Errorf("\ngot :%q\nwant:%q\nAS TEXT\ngot\n%swant\n%s", got, want, rslt[0], rslt[1]) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/asset_packaging/.gitignore: -------------------------------------------------------------------------------- 1 | *_vfsdata.go 2 | app 3 | -------------------------------------------------------------------------------- /examples/asset_packaging/Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go run main.go 3 | 4 | deps: 5 | go get -u github.com/shurcooL/vfsgen 6 | 7 | build: 8 | go generate 9 | go build -tags=deploy_build -o bin/app main.go 10 | 11 | .PHONY: run build deps 12 | -------------------------------------------------------------------------------- /examples/asset_packaging/README.md: -------------------------------------------------------------------------------- 1 | # Asset packaging example 2 | 3 | This example demonstrates how to package up your templates into your app. This is useful for your web app deployment to only consist of copying over the compiled binary. 4 | 5 | To cut down on the complexity of this example packaging up your public folder and other local assets is not shown but the process is easily extensible to incorporate these files and folders as well. 6 | 7 | To see this project in action: 8 | 9 | ``` 10 | $ go get -u "github.com/shurcooL/vfsgen" 11 | $ make build 12 | ``` 13 | 14 | That will build the app while compiling in the views folder in this directory. To be sure, move the compiled binary to another location, then run it from there. Access http://localhost:9090 in your browser and see that it works regardless of location. 15 | 16 | Local development is also possible. Do a `make run` in this directory, change something in the templates, refresh the browser and see it reflected there. This example is set up to use the local files in development as well as having Jet's development mode on which doesn't cache the templates – disabling the development mode when running in production is about the only thing not covered in this example because it'll depend on your app and its configuration on how this is done. 17 | 18 | Finally, for anyone looking for a step-by-step guide on how this is accomplished: 19 | 20 | 1. Add `github.com/shurcooL/vfsgen` to your project (vendoring is encouraged) 21 | 2. Add the `assets/generate.go` and `assets/templates/templates.go` files (copy the contents from this project) 22 | 3. Add `//go:generate go run assets/generate.go` to your `main.go` file (above `package main`) 23 | 4. Add a build target to your Makefile like you see in this project. 24 | 25 | Here's the rundown: when the Makefile target executes, it will first run `go generate`. This will look through the Go files in the current directory and search for annotations like you added above: `//go:generate` and run the command there. That runs the asset generation through `vfsgen` and generates the `templates_vfsdata.go` file you see when the build finishes. Through some build tags that are only set on this build (`deploy_build` in this case), only that file is included in the binary and that contains the view files as binary data. 26 | 27 | The last thing is to configure the Jet template engine via the multi loader to also use that `http.FileSystem` to look for templates – that's done in the `main.go` file. 28 | 29 | This is it, the templates are now loaded from within the binary. This process can be extended to include more directory trees – just add another folder to the assets directory, configure vfsgen in the generate.go file to fetch that directory tree and you're done. We did that in our projects with locale files as well as the whole public folder. As Gophers like to say: Just One Binary™. 30 | -------------------------------------------------------------------------------- /examples/asset_packaging/assets/generate.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/shurcooL/vfsgen" 12 | ) 13 | 14 | func main() { 15 | var cwd, _ = os.Getwd() 16 | templates := http.Dir(filepath.Join(cwd, "views")) 17 | if err := vfsgen.Generate(templates, vfsgen.Options{ 18 | Filename: "assets/templates/templates_vfsdata.go", 19 | PackageName: "templates", 20 | BuildTags: "deploy_build", 21 | VariableName: "Assets", 22 | }); err != nil { 23 | log.Fatalln(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/asset_packaging/assets/templates/templates.go: -------------------------------------------------------------------------------- 1 | // +build !deploy_build 2 | 3 | package templates 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | // Assets is not used in development and is always nil. 10 | var Assets http.FileSystem 11 | -------------------------------------------------------------------------------- /examples/asset_packaging/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +Build ignore 16 | //go:generate go run assets/generate.go 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "flag" 22 | "fmt" 23 | "io/ioutil" 24 | "log" 25 | "net/http" 26 | "os" 27 | "strings" 28 | "time" 29 | 30 | "github.com/CloudyKit/jet/v6" 31 | "github.com/CloudyKit/jet/v6/examples/asset_packaging/assets/templates" 32 | "github.com/CloudyKit/jet/v6/loaders/httpfs" 33 | ) 34 | 35 | var views *jet.Set 36 | 37 | func init() { 38 | httpfsLoader, err := httpfs.NewLoader(templates.Assets) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | views = jet.NewSet( 44 | httpfsLoader, 45 | jet.DevelopmentMode(true), // remove or set false in production 46 | ) 47 | } 48 | 49 | var runAndExit = flag.Bool("run-and-exit", false, "Run app, request / and exit (used in tests)") 50 | 51 | func main() { 52 | flag.Parse() 53 | 54 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 55 | view, err := views.GetTemplate("index.jet") 56 | if err != nil { 57 | w.WriteHeader(503) 58 | fmt.Fprintf(w, "Unexpected error while parsing template: %+v", err.Error()) 59 | return 60 | } 61 | var resp bytes.Buffer 62 | if err = view.Execute(&resp, nil, nil); err != nil { 63 | w.WriteHeader(503) 64 | fmt.Fprintf(w, "Error when executing template: %+v", err.Error()) 65 | return 66 | } 67 | w.WriteHeader(200) 68 | w.Write(resp.Bytes()) 69 | }) 70 | 71 | port := os.Getenv("PORT") 72 | if len(port) == 0 { 73 | port = ":9090" 74 | } else if !strings.HasPrefix(":", port) { 75 | port = ":" + port 76 | } 77 | 78 | log.Println("Serving on " + port) 79 | if *runAndExit { 80 | go http.ListenAndServe(port, nil) 81 | time.Sleep(1000) // wait for the server to be up 82 | resp, err := http.Get("http://localhost" + port + "/") 83 | if err != nil || resp.StatusCode != 200 { 84 | r, _ := ioutil.ReadAll(resp.Body) 85 | log.Printf("An error occurred when fetching page: %+v\n\nResponse:\n%+v\n\nStatus code: %v\n", err, string(r), resp.StatusCode) 86 | os.Exit(1) 87 | } 88 | os.Exit(0) 89 | } 90 | 91 | http.ListenAndServe(port, nil) 92 | } 93 | -------------------------------------------------------------------------------- /examples/asset_packaging/views/includes/_partial.jet: -------------------------------------------------------------------------------- 1 |

Included partial

2 | -------------------------------------------------------------------------------- /examples/asset_packaging/views/includes/blocks.jet: -------------------------------------------------------------------------------- 1 | {{block menu()}} 2 |

The menu block was invoked.

3 | {{end}} 4 | -------------------------------------------------------------------------------- /examples/asset_packaging/views/index.jet: -------------------------------------------------------------------------------- 1 | {{extends "layouts/application.jet"}} 2 | {{import "includes/blocks.jet"}} 3 | 4 | {{block documentBody()}} 5 |

Asset Packaging example

6 | 7 | 10 | 11 | {{include "includes/_partial.jet"}} 12 | 13 | {{if !includeIfExists("doesNotExist.jet")}} 14 |

doesNotExist.jet was not included because it doesn't exist.

15 | {{end}} 16 | {{end}} 17 | -------------------------------------------------------------------------------- /examples/asset_packaging/views/layouts/application.jet: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Asset packaging example 6 | 7 | 8 | {{block documentBody()}}{{end}} 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/todos/devop.yml: -------------------------------------------------------------------------------- 1 | gobuild: 2 | match: "\\.go$" 3 | command: "go build ." 4 | continue: gorun 5 | wait: true 6 | stderr: true 7 | stdout: true 8 | gorun: 9 | command: "./example" 10 | env: 11 | - PORT=:8890 12 | stderr: true 13 | stdout: true -------------------------------------------------------------------------------- /examples/todos/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +Build ignore 16 | package main 17 | 18 | import ( 19 | "bytes" 20 | "encoding/base64" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | "os" 25 | "reflect" 26 | "strings" 27 | 28 | "github.com/CloudyKit/jet/v6" 29 | ) 30 | 31 | var views = jet.NewSet( 32 | jet.NewOSFileSystemLoader("./views"), 33 | jet.DevelopmentMode(true), // remove or set false in production 34 | ) 35 | 36 | type tTODO struct { 37 | Text string 38 | Done bool 39 | } 40 | 41 | type doneTODOs struct { 42 | list map[string]*tTODO 43 | keys []string 44 | len int 45 | i int 46 | } 47 | 48 | func (dt *doneTODOs) New(todos map[string]*tTODO) *doneTODOs { 49 | dt.len = len(todos) 50 | for k := range todos { 51 | dt.keys = append(dt.keys, k) 52 | } 53 | dt.list = todos 54 | return dt 55 | } 56 | 57 | // Range satisfies the jet.Ranger interface and only returns TODOs that are done, 58 | // even when the list contains TODOs that are not done. 59 | func (dt *doneTODOs) Range() (reflect.Value, reflect.Value, bool) { 60 | for dt.i < dt.len { 61 | key := dt.keys[dt.i] 62 | dt.i++ 63 | if dt.list[key].Done { 64 | return reflect.ValueOf(key), reflect.ValueOf(dt.list[key]), false 65 | } 66 | } 67 | return reflect.Value{}, reflect.Value{}, true 68 | } 69 | 70 | func (dt *doneTODOs) ProvidesIndex() bool { return true } 71 | 72 | // Render implements jet.Renderer interface 73 | func (t *tTODO) Render(r *jet.Runtime) { 74 | done := "yes" 75 | if !t.Done { 76 | done = "no" 77 | } 78 | r.Write([]byte(fmt.Sprintf("TODO: %s (done: %s)", t.Text, done))) 79 | } 80 | 81 | func main() { 82 | views.AddGlobalFunc("base64", func(a jet.Arguments) reflect.Value { 83 | a.RequireNumOfArguments("base64", 1, 1) 84 | 85 | buffer := bytes.NewBuffer(nil) 86 | fmt.Fprint(buffer, a.Get(0)) 87 | 88 | return reflect.ValueOf(base64.URLEncoding.EncodeToString(buffer.Bytes())) 89 | }) 90 | var todos = map[string]*tTODO{ 91 | "example-todo-1": &tTODO{Text: "Add an show todo page to the example project", Done: true}, 92 | "example-todo-2": &tTODO{Text: "Add an add todo page to the example project"}, 93 | "example-todo-3": &tTODO{Text: "Add an update todo page to the example project"}, 94 | "example-todo-4": &tTODO{Text: "Add an delete todo page to the example project", Done: true}, 95 | } 96 | 97 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 98 | view, err := views.GetTemplate("todos/index.jet") 99 | if err != nil { 100 | log.Println("Unexpected template err:", err.Error()) 101 | } 102 | view.Execute(w, nil, todos) 103 | }) 104 | http.HandleFunc("/todo", func(w http.ResponseWriter, r *http.Request) { 105 | view, err := views.GetTemplate("todos/show.jet") 106 | if err != nil { 107 | log.Println("Unexpected template err:", err.Error()) 108 | } 109 | id := r.URL.Query().Get("id") 110 | todo, ok := todos[id] 111 | if !ok { 112 | http.Redirect(w, r, "/", http.StatusNotFound) 113 | } 114 | view.Execute(w, nil, todo) 115 | }) 116 | http.HandleFunc("/all-done", func(w http.ResponseWriter, r *http.Request) { 117 | view, err := views.GetTemplate("todos/index.jet") 118 | if err != nil { 119 | log.Println("Unexpected template err:", err.Error()) 120 | } 121 | vars := make(jet.VarMap) 122 | vars.Set("showingAllDone", true) 123 | view.Execute(w, vars, (&doneTODOs{}).New(todos)) 124 | }) 125 | 126 | port := os.Getenv("PORT") 127 | if len(port) == 0 { 128 | port = ":8080" 129 | } else if !strings.HasPrefix(":", port) { 130 | port = ":" + port 131 | } 132 | 133 | log.Println("Serving on " + port) 134 | http.ListenAndServe(port, nil) 135 | } 136 | -------------------------------------------------------------------------------- /examples/todos/views/layouts/application.jet: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ isset(title) ? title : "" }} 6 | 7 | 8 | {{block documentBody()}}{{end}} 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/todos/views/todos/index.jet: -------------------------------------------------------------------------------- 1 | {{extends "../layouts/application.jet"}} 2 | 3 | {{block button(label, href="javascript:void(0)")}} 4 | {{ label }} 5 | {{end}} 6 | 7 | {{block ul()}} 8 |
    9 | {{yield content}} 10 |
11 | {{end}} 12 | 13 | {{block documentBody()}} 14 |

List of TODOs

15 | {{if isset(showingAllDone) && showingAllDone}} 16 |

Showing only TODOs that are done

17 | {{else}} 18 |

Show only TODOs that are done

19 | {{end}} 20 | 21 | {{yield ul() content}} 22 | {{range id, value := .}} 23 |
  • 24 | {{ value.Text }} 25 | {{yield button(label="UP", href="/update/?id="+base64(id))}} - {{yield button(href="/delete/?id="+id, label="DL")}} 26 |
  • 27 | {{end}} 28 | {{end}} 29 | {{end}} 30 | -------------------------------------------------------------------------------- /examples/todos/views/todos/show.jet: -------------------------------------------------------------------------------- 1 | {{extends "../layouts/application.jet"}} 2 | 3 | {{block documentBody()}} 4 |

    Show TODO

    5 |

    This uses a custom renderer by implementing the jet.Renderer interface. 6 |

    7 | {{ . }} 8 |

    9 | {{end}} 10 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Jet is a fast and dynamic template engine for the Go programming language, set of features 16 | // includes very fast template execution, a dynamic and flexible language, template inheritance, low number of allocations, 17 | // special interfaces to allow even further optimizations. 18 | 19 | package jet 20 | 21 | import ( 22 | "io" 23 | "reflect" 24 | "sort" 25 | ) 26 | 27 | type VarMap map[string]reflect.Value 28 | 29 | // SortedKeys returns a sorted slice of VarMap keys 30 | func (scope VarMap) SortedKeys() []string { 31 | keys := make([]string, 0, len(scope)) 32 | for k := range scope { 33 | keys = append(keys, k) 34 | } 35 | sort.Strings(keys) 36 | return keys 37 | } 38 | 39 | func (scope VarMap) Set(name string, v interface{}) VarMap { 40 | scope[name] = reflect.ValueOf(v) 41 | return scope 42 | } 43 | 44 | func (scope VarMap) SetFunc(name string, v Func) VarMap { 45 | scope[name] = reflect.ValueOf(v) 46 | return scope 47 | } 48 | 49 | func (scope VarMap) SetWriter(name string, v SafeWriter) VarMap { 50 | scope[name] = reflect.ValueOf(v) 51 | return scope 52 | } 53 | 54 | // Execute executes the template into w. 55 | func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) (err error) { 56 | st := pool_State.Get().(*Runtime) 57 | defer st.recover(&err) 58 | 59 | st.blocks = t.processedBlocks 60 | st.variables = variables 61 | st.set = t.set 62 | st.Writer = w 63 | 64 | // resolve extended template 65 | for t.extends != nil { 66 | t = t.extends 67 | } 68 | 69 | if data != nil { 70 | st.context = reflect.ValueOf(data) 71 | } 72 | 73 | st.executeList(t.Root) 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /exec_test.go: -------------------------------------------------------------------------------- 1 | package jet 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestExecuteConcurrency(t *testing.T) { 10 | l := NewInMemLoader() 11 | l.Set("foo", "{{if true}}Hi {{ .Name }}!{{end}}") 12 | 13 | set := NewSet(l) 14 | 15 | tpl, err := set.GetTemplate("foo") 16 | if err != nil { 17 | t.Errorf("getting template from set: %v", err) 18 | } 19 | 20 | for i := 0; i < 100; i++ { 21 | t.Run(fmt.Sprintf("CC_%d", i), func(t *testing.T) { 22 | t.Parallel() 23 | 24 | err := tpl.Execute(ioutil.Discard, nil, struct{ Name string }{Name: "John"}) 25 | if err != nil { 26 | t.Errorf("executing template: %v", err) 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /func.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "time" 21 | ) 22 | 23 | // Arguments holds the arguments passed to jet.Func. 24 | type Arguments struct { 25 | runtime *Runtime 26 | args CallArgs 27 | pipedVal *reflect.Value 28 | } 29 | 30 | // IsSet checks whether an argument is set or not. It behaves like the build-in isset function. 31 | func (a *Arguments) IsSet(argumentIndex int) bool { 32 | if argumentIndex < 0 { 33 | return false 34 | } 35 | 36 | if a.pipedVal != nil && !a.args.HasPipeSlot { 37 | if argumentIndex == 0 { 38 | return true 39 | } 40 | // call has an implicit first argument, so we adjust the 41 | // index before looking it up in the parsed a.args slice 42 | argumentIndex-- 43 | } 44 | 45 | if argumentIndex < len(a.args.Exprs) { 46 | e := a.args.Exprs[argumentIndex] 47 | switch e.Type() { 48 | case NodeUnderscore: 49 | return a.pipedVal != nil 50 | default: 51 | return a.runtime.isSet(e) 52 | } 53 | } 54 | 55 | return false 56 | } 57 | 58 | // Get gets an argument by index. 59 | func (a *Arguments) Get(argumentIndex int) reflect.Value { 60 | if argumentIndex < 0 { 61 | return reflect.Value{} 62 | } 63 | 64 | if a.pipedVal != nil && !a.args.HasPipeSlot { 65 | if argumentIndex == 0 { 66 | return *a.pipedVal 67 | } 68 | // call has an implicit first argument, so we adjust the 69 | // index before looking it up in the parsed a.args slice 70 | argumentIndex-- 71 | } 72 | 73 | if argumentIndex < len(a.args.Exprs) { 74 | e := a.args.Exprs[argumentIndex] 75 | switch e.Type() { 76 | case NodeUnderscore: 77 | return *a.pipedVal 78 | default: 79 | return a.runtime.evalPrimaryExpressionGroup(e) 80 | } 81 | } 82 | 83 | return reflect.Value{} 84 | } 85 | 86 | // Panicf panics with formatted error message. 87 | func (a *Arguments) Panicf(format string, v ...interface{}) { 88 | panic(fmt.Errorf(format, v...)) 89 | } 90 | 91 | // RequireNumOfArguments panics if the number of arguments is not in the range specified by min and max. 92 | // In case there is no minimum pass -1, in case there is no maximum pass -1 respectively. 93 | func (a *Arguments) RequireNumOfArguments(funcname string, min, max int) { 94 | num := a.NumOfArguments() 95 | if min >= 0 && num < min { 96 | a.Panicf("unexpected number of arguments in a call to %s", funcname) 97 | } else if max >= 0 && num > max { 98 | a.Panicf("unexpected number of arguments in a call to %s", funcname) 99 | } 100 | } 101 | 102 | // NumOfArguments returns the number of arguments 103 | func (a *Arguments) NumOfArguments() int { 104 | num := len(a.args.Exprs) 105 | if a.pipedVal != nil && !a.args.HasPipeSlot { 106 | return num + 1 107 | } 108 | return num 109 | } 110 | 111 | // Runtime get the Runtime context 112 | func (a *Arguments) Runtime() *Runtime { 113 | return a.runtime 114 | } 115 | 116 | // ParseInto parses the arguments into the provided pointers. It returns an error if the number of pointers passed in does not 117 | // equal the number of arguments, if any argument's value is invalid according to Go's reflect package, if an argument can't 118 | // be used as the value the pointer passed in at the corresponding position points to, or if an unhandled pointer type is encountered. 119 | // Allowed pointer types are pointers to interface{}, int, int64, float64, bool, string, time.Time, reflect.Value, []interface{}, 120 | // map[string]interface{}. If a pointer to a reflect.Value is passed in, the argument be assigned as-is to the value pointed to. For 121 | // pointers to int or float types, type conversion is performed automatically if necessary. 122 | func (a *Arguments) ParseInto(ptrs ...interface{}) error { 123 | if len(ptrs) < a.NumOfArguments() { 124 | return fmt.Errorf("have %d arguments, but only %d pointers to parse into", a.NumOfArguments(), len(ptrs)) 125 | } 126 | 127 | for i := 0; i < a.NumOfArguments(); i++ { 128 | arg, ptr := indirectEface(a.Get(i)), ptrs[i] 129 | ok := false 130 | 131 | if !arg.IsValid() { 132 | return fmt.Errorf("argument at position %d is not a valid value", i) 133 | } 134 | 135 | switch p := ptr.(type) { 136 | case *reflect.Value: 137 | *p, ok = arg, true 138 | case *int: 139 | switch arg.Kind() { 140 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 141 | *p, ok = int(arg.Int()), true 142 | case reflect.Float32, reflect.Float64: 143 | *p, ok = int(arg.Float()), true 144 | default: 145 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr) 146 | } 147 | case *int64: 148 | switch arg.Kind() { 149 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 150 | *p, ok = arg.Int(), true 151 | case reflect.Float32, reflect.Float64: 152 | *p, ok = int64(arg.Float()), true 153 | default: 154 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr) 155 | } 156 | case *float64: 157 | switch arg.Kind() { 158 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 159 | *p, ok = float64(arg.Int()), true 160 | case reflect.Float32, reflect.Float64: 161 | *p, ok = arg.Float(), true 162 | default: 163 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr) 164 | } 165 | } 166 | 167 | if ok { 168 | continue 169 | } 170 | 171 | if !arg.CanInterface() { 172 | return fmt.Errorf("argument at position %d can't be accessed via Interface()", i) 173 | } 174 | val := arg.Interface() 175 | 176 | switch p := ptr.(type) { 177 | case *interface{}: 178 | *p, ok = val, true 179 | case *bool: 180 | *p, ok = val.(bool) 181 | case *string: 182 | *p, ok = val.(string) 183 | case *time.Time: 184 | *p, ok = val.(time.Time) 185 | case *[]interface{}: 186 | *p, ok = val.([]interface{}) 187 | case *map[string]interface{}: 188 | *p, ok = val.(map[string]interface{}) 189 | default: 190 | return fmt.Errorf("trying to parse %v into %v: unhandled value type %T", arg, p, val) 191 | } 192 | 193 | if !ok { 194 | return fmt.Errorf("could not parse %v (%s) into %v (%T)", arg, arg.Type(), ptr, ptr) 195 | } 196 | } 197 | 198 | return nil 199 | } 200 | 201 | // Func function implementing this type is called directly, which is faster than calling through reflect. 202 | // If a function is being called many times in the execution of a template, you may consider implementing 203 | // a wrapper for that function implementing a Func. 204 | type Func func(Arguments) reflect.Value 205 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CloudyKit/jet/v6 2 | 3 | go 1.16 4 | 5 | require github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= 2 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= 3 | -------------------------------------------------------------------------------- /jettest/test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jettest 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/CloudyKit/jet/v6" 24 | ) 25 | 26 | func RunWithSet(t *testing.T, set *jet.Set, variables jet.VarMap, context interface{}, testName, testExpected string) { 27 | tt, err := set.GetTemplate(testName) 28 | if err != nil { 29 | t.Errorf("Error parsing templates for test %s: %v", testName, err) 30 | return 31 | } 32 | RunWithTemplate(t, tt, variables, context, testExpected) 33 | } 34 | 35 | func RunWithTemplate(t *testing.T, tt *jet.Template, variables jet.VarMap, context interface{}, testExpected string) { 36 | if testing.RunTests(func(pat, str string) (bool, error) { 37 | return true, nil 38 | }, []testing.InternalTest{ 39 | { 40 | Name: fmt.Sprintf("\tJetTest(%s)", tt.Name), 41 | F: func(t *testing.T) { 42 | var buf bytes.Buffer 43 | err := tt.Execute(&buf, variables, context) 44 | if err != nil { 45 | t.Errorf("Eval error: %q executing %s", err.Error(), tt.Name) 46 | return 47 | } 48 | result := strings.Replace(buf.String(), "\r\n", "\n", -1) 49 | if result != testExpected { 50 | t.Errorf("Result error expected %q got %q on %s", testExpected, result, tt.Name) 51 | } 52 | }, 53 | }, 54 | }) == false { 55 | t.Fail() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lex.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | "unicode" 21 | "unicode/utf8" 22 | ) 23 | 24 | // item represents a token or text string returned from the scanner. 25 | type item struct { 26 | typ itemType // The type of this item. 27 | pos Pos // The starting position, in bytes, of this item in the input string. 28 | val string // The value of this item. 29 | } 30 | 31 | func (i item) String() string { 32 | switch { 33 | case i.typ == itemEOF: 34 | return "EOF" 35 | case i.typ == itemError: 36 | return i.val 37 | case i.typ > itemKeyword: 38 | return fmt.Sprintf("<%s>", i.val) 39 | case len(i.val) > 10: 40 | return fmt.Sprintf("%.10q...", i.val) 41 | } 42 | return fmt.Sprintf("%q", i.val) 43 | } 44 | 45 | // itemType identifies the type of lex items. 46 | type itemType int 47 | 48 | const ( 49 | itemError itemType = iota // error occurred; value is text of error 50 | itemBool // boolean constant 51 | itemChar // printable ASCII character; grab bag for comma etc. 52 | itemCharConstant // character constant 53 | itemComplex // complex constant (1+2i); imaginary is just a number 54 | itemEOF 55 | itemField // alphanumeric identifier starting with '.' 56 | itemIdentifier // alphanumeric identifier not starting with '.' 57 | itemLeftDelim // left action delimiter 58 | itemLeftParen // '(' inside action 59 | itemNumber // simple number, including imaginary 60 | itemPipe // pipe symbol 61 | itemRawString // raw quoted string (includes quotes) 62 | itemRightDelim // right action delimiter 63 | itemRightParen // ')' inside action 64 | itemSpace // run of spaces separating arguments 65 | itemString // quoted string (includes quotes) 66 | itemText // plain text 67 | itemAssign 68 | itemEquals 69 | itemNotEquals 70 | itemGreat 71 | itemGreatEquals 72 | itemLess 73 | itemLessEquals 74 | itemComma 75 | itemSemicolon 76 | itemAdd 77 | itemMinus 78 | itemMul 79 | itemDiv 80 | itemMod 81 | itemColon 82 | itemTernary 83 | itemLeftBrackets 84 | itemRightBrackets 85 | itemUnderscore 86 | // Keywords appear after all the rest. 87 | itemKeyword // used only to delimit the keywords 88 | itemExtends 89 | itemImport 90 | itemInclude 91 | itemBlock 92 | itemEnd 93 | itemYield 94 | itemContent 95 | itemIf 96 | itemElse 97 | itemRange 98 | itemTry 99 | itemCatch 100 | itemReturn 101 | itemAnd 102 | itemOr 103 | itemNot 104 | itemNil 105 | itemMSG 106 | itemTrans 107 | ) 108 | 109 | var key = map[string]itemType{ 110 | "extends": itemExtends, 111 | "import": itemImport, 112 | 113 | "include": itemInclude, 114 | "block": itemBlock, 115 | "end": itemEnd, 116 | "yield": itemYield, 117 | "content": itemContent, 118 | 119 | "if": itemIf, 120 | "else": itemElse, 121 | 122 | "range": itemRange, 123 | 124 | "try": itemTry, 125 | "catch": itemCatch, 126 | 127 | "return": itemReturn, 128 | 129 | "and": itemAnd, 130 | "or": itemOr, 131 | "not": itemNot, 132 | 133 | "nil": itemNil, 134 | 135 | "msg": itemMSG, 136 | "trans": itemTrans, 137 | } 138 | 139 | const eof = -1 140 | 141 | const ( 142 | defaultLeftDelim = "{{" 143 | defaultRightDelim = "}}" 144 | defaultLeftComment = "{*" 145 | defaultRightComment = "*}" 146 | leftTrimMarker = "- " 147 | rightTrimMarker = " -" 148 | trimMarkerLen = Pos(len(leftTrimMarker)) 149 | ) 150 | 151 | // stateFn represents the state of the scanner as a function that returns the next state. 152 | type stateFn func(*lexer) stateFn 153 | 154 | // lexer holds the state of the scanner. 155 | type lexer struct { 156 | name string // the name of the input; used only for error reports 157 | input string // the string being scanned 158 | state stateFn // the next lexing function to enter 159 | pos Pos // current position in the input 160 | start Pos // start position of this item 161 | width Pos // width of last rune read from input 162 | lastPos Pos // position of most recent item returned by nextItem 163 | items chan item // channel of scanned items 164 | parenDepth int // nesting depth of ( ) exprs 165 | lastType itemType 166 | leftDelim string 167 | rightDelim string 168 | leftComment string 169 | rightComment string 170 | trimRightDelim string 171 | } 172 | 173 | func (l *lexer) setDelimiters(leftDelim, rightDelim string) { 174 | if leftDelim != "" { 175 | l.leftDelim = leftDelim 176 | } 177 | if rightDelim != "" { 178 | l.rightDelim = rightDelim 179 | } 180 | } 181 | 182 | func (l *lexer) setCommentDelimiters(leftDelim, rightDelim string) { 183 | if leftDelim != "" { 184 | l.leftComment = leftDelim 185 | } 186 | if rightDelim != "" { 187 | l.rightComment = rightDelim 188 | } 189 | } 190 | 191 | // next returns the next rune in the input. 192 | func (l *lexer) next() rune { 193 | if int(l.pos) >= len(l.input) { 194 | l.width = 0 195 | return eof 196 | } 197 | r, w := utf8.DecodeRuneInString(l.input[l.pos:]) 198 | l.width = Pos(w) 199 | l.pos += l.width 200 | return r 201 | } 202 | 203 | // peek returns but does not consume the next rune in the input. 204 | func (l *lexer) peek() rune { 205 | r := l.next() 206 | l.backup() 207 | return r 208 | } 209 | 210 | // backup steps back one rune. Can only be called once per call of next. 211 | func (l *lexer) backup() { 212 | l.pos -= l.width 213 | } 214 | 215 | // emit passes an item back to the client. 216 | func (l *lexer) emit(t itemType) { 217 | l.lastType = t 218 | l.items <- item{t, l.start, l.input[l.start:l.pos]} 219 | l.start = l.pos 220 | } 221 | 222 | // ignore skips over the pending input before this point. 223 | func (l *lexer) ignore() { 224 | l.start = l.pos 225 | } 226 | 227 | // accept consumes the next rune if it's from the valid set. 228 | func (l *lexer) accept(valid string) bool { 229 | if strings.IndexRune(valid, l.next()) >= 0 { 230 | return true 231 | } 232 | l.backup() 233 | return false 234 | } 235 | 236 | // acceptRun consumes a run of runes from the valid set. 237 | func (l *lexer) acceptRun(valid string) { 238 | for strings.IndexRune(valid, l.next()) >= 0 { 239 | } 240 | l.backup() 241 | } 242 | 243 | // lineNumber reports which line we're on, based on the position of 244 | // the previous item returned by nextItem. Doing it this way 245 | // means we don't have to worry about peek double counting. 246 | func (l *lexer) lineNumber() int { 247 | return 1 + strings.Count(l.input[:l.lastPos], "\n") 248 | } 249 | 250 | // errorf returns an error token and terminates the scan by passing 251 | // back a nil pointer that will be the next state, terminating l.nextItem. 252 | func (l *lexer) errorf(format string, args ...interface{}) stateFn { 253 | l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} 254 | return nil 255 | } 256 | 257 | // nextItem returns the next item from the input. 258 | // Called by the parser, not in the lexing goroutine. 259 | func (l *lexer) nextItem() item { 260 | item := <-l.items 261 | l.lastPos = item.pos 262 | return item 263 | } 264 | 265 | // drain drains the output so the lexing goroutine will exit. 266 | // Called by the parser, not in the lexing goroutine. 267 | func (l *lexer) drain() { 268 | for range l.items { 269 | } 270 | } 271 | 272 | // lex creates a new scanner for the input string. 273 | func lex(name, input string, run bool) *lexer { 274 | l := &lexer{ 275 | name: name, 276 | input: input, 277 | items: make(chan item), 278 | leftDelim: defaultLeftDelim, 279 | rightDelim: defaultRightDelim, 280 | leftComment: defaultLeftComment, 281 | rightComment: defaultRightComment, 282 | trimRightDelim: rightTrimMarker + defaultRightDelim, 283 | } 284 | if run { 285 | l.run() 286 | } 287 | return l 288 | } 289 | 290 | // run runs the state machine for the lexer. 291 | func (l *lexer) run() { 292 | go func() { 293 | for l.state = lexText; l.state != nil; { 294 | l.state = l.state(l) 295 | } 296 | close(l.items) 297 | }() 298 | } 299 | 300 | // state functions 301 | func lexText(l *lexer) stateFn { 302 | for { 303 | // without breaking the API, this seems like a reasonable workaround to correctly parse comments 304 | i := strings.IndexByte(l.input[l.pos:], l.leftDelim[0]) // index of suspected left delimiter 305 | ic := strings.IndexByte(l.input[l.pos:], l.leftComment[0]) // index of suspected left comment marker 306 | if ic > -1 && ic < i { // use whichever is lower for future lexing 307 | i = ic 308 | } 309 | // if no token is found, skip till the end of template 310 | if i == -1 { 311 | l.pos = Pos(len(l.input)) 312 | break 313 | } else { 314 | l.pos += Pos(i) 315 | if strings.HasPrefix(l.input[l.pos:], l.leftDelim) { 316 | ld := Pos(len(l.leftDelim)) 317 | trimLength := Pos(0) 318 | if strings.HasPrefix(l.input[l.pos+ld:], leftTrimMarker) { 319 | trimLength = rightTrimLength(l.input[l.start:l.pos]) 320 | } 321 | l.pos -= trimLength 322 | if l.pos > l.start { 323 | l.emit(itemText) 324 | } 325 | l.pos += trimLength 326 | l.ignore() 327 | return lexLeftDelim 328 | } 329 | if strings.HasPrefix(l.input[l.pos:], l.leftComment) { 330 | if l.pos > l.start { 331 | l.emit(itemText) 332 | } 333 | return lexComment 334 | } 335 | } 336 | if l.next() == eof { 337 | break 338 | } 339 | } 340 | // Correctly reached EOF. 341 | if l.pos > l.start { 342 | l.emit(itemText) 343 | } 344 | l.emit(itemEOF) 345 | return nil 346 | } 347 | 348 | func lexLeftDelim(l *lexer) stateFn { 349 | l.pos += Pos(len(l.leftDelim)) 350 | l.emit(itemLeftDelim) 351 | trimSpace := strings.HasPrefix(l.input[l.pos:], leftTrimMarker) 352 | if trimSpace { 353 | l.pos += trimMarkerLen 354 | l.ignore() 355 | } 356 | l.parenDepth = 0 357 | return lexInsideAction 358 | } 359 | 360 | // lexComment scans a comment. The left comment marker is known to be present. 361 | func lexComment(l *lexer) stateFn { 362 | l.pos += Pos(len(l.leftComment)) 363 | i := strings.Index(l.input[l.pos:], l.rightComment) 364 | if i < 0 { 365 | return l.errorf("unclosed comment") 366 | } 367 | l.pos += Pos(i + len(l.rightComment)) 368 | l.ignore() 369 | return lexText 370 | } 371 | 372 | // lexRightDelim scans the right delimiter, which is known to be present. 373 | func lexRightDelim(l *lexer) stateFn { 374 | trimSpace := strings.HasPrefix(l.input[l.pos:], rightTrimMarker) 375 | if trimSpace { 376 | l.pos += trimMarkerLen 377 | l.ignore() 378 | } 379 | l.pos += Pos(len(l.rightDelim)) 380 | l.emit(itemRightDelim) 381 | if trimSpace { 382 | l.pos += leftTrimLength(l.input[l.pos:]) 383 | l.ignore() 384 | } 385 | return lexText 386 | } 387 | 388 | // lexInsideAction scans the elements inside action delimiters. 389 | func lexInsideAction(l *lexer) stateFn { 390 | // Either number, quoted string, or identifier. 391 | // Spaces separate arguments; runs of spaces turn into itemSpace. 392 | // Pipe symbols separate and are emitted. 393 | delim, _ := l.atRightDelim() 394 | if delim { 395 | if l.parenDepth == 0 { 396 | return lexRightDelim 397 | } 398 | return l.errorf("unclosed left parenthesis") 399 | } 400 | switch r := l.next(); { 401 | case r == eof: 402 | return l.errorf("unclosed action") 403 | case isSpace(r): 404 | return lexSpace 405 | case r == ',': 406 | l.emit(itemComma) 407 | case r == ';': 408 | l.emit(itemSemicolon) 409 | case r == '*': 410 | l.emit(itemMul) 411 | case r == '/': 412 | l.emit(itemDiv) 413 | case r == '%': 414 | l.emit(itemMod) 415 | case r == '-': 416 | 417 | if r := l.peek(); '0' <= r && r <= '9' && 418 | itemAdd != l.lastType && 419 | itemMinus != l.lastType && 420 | itemNumber != l.lastType && 421 | itemIdentifier != l.lastType && 422 | itemString != l.lastType && 423 | itemRawString != l.lastType && 424 | itemCharConstant != l.lastType && 425 | itemBool != l.lastType && 426 | itemField != l.lastType && 427 | itemChar != l.lastType && 428 | itemTrans != l.lastType { 429 | l.backup() 430 | return lexNumber 431 | } 432 | l.emit(itemMinus) 433 | case r == '+': 434 | if r := l.peek(); '0' <= r && r <= '9' && 435 | itemAdd != l.lastType && 436 | itemMinus != l.lastType && 437 | itemNumber != l.lastType && 438 | itemIdentifier != l.lastType && 439 | itemString != l.lastType && 440 | itemRawString != l.lastType && 441 | itemCharConstant != l.lastType && 442 | itemBool != l.lastType && 443 | itemField != l.lastType && 444 | itemChar != l.lastType && 445 | itemTrans != l.lastType { 446 | l.backup() 447 | return lexNumber 448 | } 449 | l.emit(itemAdd) 450 | case r == '?': 451 | l.emit(itemTernary) 452 | case r == '&': 453 | if l.next() == '&' { 454 | l.emit(itemAnd) 455 | } else { 456 | l.backup() 457 | } 458 | case r == '<': 459 | if l.next() == '=' { 460 | l.emit(itemLessEquals) 461 | } else { 462 | l.backup() 463 | l.emit(itemLess) 464 | } 465 | case r == '>': 466 | if l.next() == '=' { 467 | l.emit(itemGreatEquals) 468 | } else { 469 | l.backup() 470 | l.emit(itemGreat) 471 | } 472 | case r == '!': 473 | if l.next() == '=' { 474 | l.emit(itemNotEquals) 475 | } else { 476 | l.backup() 477 | l.emit(itemNot) 478 | } 479 | 480 | case r == '=': 481 | if l.next() == '=' { 482 | l.emit(itemEquals) 483 | } else { 484 | l.backup() 485 | l.emit(itemAssign) 486 | } 487 | case r == ':': 488 | if l.next() == '=' { 489 | l.emit(itemAssign) 490 | } else { 491 | l.backup() 492 | l.emit(itemColon) 493 | } 494 | case r == '|': 495 | if l.next() == '|' { 496 | l.emit(itemOr) 497 | } else { 498 | l.backup() 499 | l.emit(itemPipe) 500 | } 501 | case r == '"': 502 | return lexQuote 503 | case r == '`': 504 | return lexRawQuote 505 | case r == '\'': 506 | return lexChar 507 | case r == '.': 508 | // special look-ahead for ".field" so we don't break l.backup(). 509 | if l.pos < Pos(len(l.input)) { 510 | r := l.input[l.pos] 511 | if r < '0' || '9' < r { 512 | return lexField 513 | } 514 | } 515 | fallthrough // '.' can start a number. 516 | case '0' <= r && r <= '9': 517 | l.backup() 518 | return lexNumber 519 | case r == '_': 520 | if !isAlphaNumeric(l.peek()) { 521 | l.emit(itemUnderscore) 522 | return lexInsideAction 523 | } 524 | fallthrough // no space? must be the start of an identifier 525 | case isAlphaNumeric(r): 526 | l.backup() 527 | return lexIdentifier 528 | case r == '[': 529 | l.emit(itemLeftBrackets) 530 | case r == ']': 531 | l.emit(itemRightBrackets) 532 | case r == '(': 533 | l.emit(itemLeftParen) 534 | l.parenDepth++ 535 | case r == ')': 536 | l.emit(itemRightParen) 537 | l.parenDepth-- 538 | if l.parenDepth < 0 { 539 | return l.errorf("unexpected right paren %#U", r) 540 | } 541 | case r <= unicode.MaxASCII && unicode.IsPrint(r): 542 | l.emit(itemChar) 543 | default: 544 | return l.errorf("unrecognized character in action: %#U", r) 545 | } 546 | return lexInsideAction 547 | } 548 | 549 | // lexSpace scans a run of space characters. 550 | // One space has already been seen. 551 | func lexSpace(l *lexer) stateFn { 552 | var numSpaces int 553 | for isSpace(l.peek()) { 554 | numSpaces++ 555 | l.next() 556 | } 557 | if strings.HasPrefix(l.input[l.pos-1:], l.trimRightDelim) { 558 | l.backup() 559 | if numSpaces == 1 { 560 | return lexRightDelim 561 | } 562 | } 563 | l.emit(itemSpace) 564 | return lexInsideAction 565 | } 566 | 567 | // lexIdentifier scans an alphanumeric. 568 | func lexIdentifier(l *lexer) stateFn { 569 | Loop: 570 | for { 571 | switch r := l.next(); { 572 | case isAlphaNumeric(r): 573 | // absorb. 574 | default: 575 | l.backup() 576 | word := l.input[l.start:l.pos] 577 | if !l.atTerminator() { 578 | return l.errorf("bad character %#U", r) 579 | } 580 | switch { 581 | case key[word] > itemKeyword: 582 | l.emit(key[word]) 583 | case word[0] == '.': 584 | l.emit(itemField) 585 | case word == "true", word == "false": 586 | l.emit(itemBool) 587 | default: 588 | l.emit(itemIdentifier) 589 | } 590 | break Loop 591 | } 592 | } 593 | return lexInsideAction 594 | } 595 | 596 | // lexField scans a field: .Alphanumeric. 597 | // The . has been scanned. 598 | func lexField(l *lexer) stateFn { 599 | 600 | if l.atTerminator() { 601 | // Nothing interesting follows -> "." or "$". 602 | l.emit(itemIdentifier) 603 | return lexInsideAction 604 | } 605 | 606 | var r rune 607 | for { 608 | r = l.next() 609 | if !isAlphaNumeric(r) { 610 | l.backup() 611 | break 612 | } 613 | } 614 | if !l.atTerminator() { 615 | return l.errorf("bad character %#U", r) 616 | } 617 | l.emit(itemField) 618 | return lexInsideAction 619 | } 620 | 621 | // atTerminator reports whether the input is at valid termination character to 622 | // appear after an identifier. Breaks .X.Y into two pieces. Also catches cases 623 | // like "$x+2" not being acceptable without a space, in case we decide one 624 | // day to implement arithmetic. 625 | func (l *lexer) atTerminator() bool { 626 | r := l.peek() 627 | if isSpace(r) { 628 | return true 629 | } 630 | switch r { 631 | case eof, '.', ',', '|', ':', ')', '=', '(', ';', '?', '[', ']', '+', '-', '/', '%', '*', '&', '!', '<', '>': 632 | return true 633 | } 634 | // Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will 635 | // succeed but should fail) but only in extremely rare cases caused by willfully 636 | // bad choice of delimiter. 637 | if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r { 638 | return true 639 | } 640 | return false 641 | } 642 | 643 | // lexChar scans a character constant. The initial quote is already 644 | // scanned. Syntax checking is done by the parser. 645 | func lexChar(l *lexer) stateFn { 646 | Loop: 647 | for { 648 | switch l.next() { 649 | case '\\': 650 | if r := l.next(); r != eof && r != '\n' { 651 | break 652 | } 653 | fallthrough 654 | case eof, '\n': 655 | return l.errorf("unterminated character constant") 656 | case '\'': 657 | break Loop 658 | } 659 | } 660 | l.emit(itemCharConstant) 661 | return lexInsideAction 662 | } 663 | 664 | // lexNumber scans a number: decimal, octal, hex, float, or imaginary. This 665 | // isn't a perfect number scanner - for instance it accepts "." and "0x0.2" 666 | // and "089" - but when it's wrong the input is invalid and the parser (via 667 | // strconv) will notice. 668 | func lexNumber(l *lexer) stateFn { 669 | if !l.scanNumber() { 670 | return l.errorf("bad number syntax: %q", l.input[l.start:l.pos]) 671 | } 672 | 673 | l.emit(itemNumber) 674 | return lexInsideAction 675 | } 676 | 677 | func (l *lexer) scanNumber() bool { 678 | // Optional leading sign. 679 | l.accept("+-") 680 | // Is it hex? 681 | digits := "0123456789" 682 | if l.accept("0") && l.accept("xX") { 683 | digits = "0123456789abcdefABCDEF" 684 | } 685 | l.acceptRun(digits) 686 | if l.accept(".") { 687 | l.acceptRun(digits) 688 | } 689 | if l.accept("eE") { 690 | l.accept("+-") 691 | l.acceptRun("0123456789") 692 | } 693 | //Is it imaginary? 694 | l.accept("i") 695 | //Next thing mustn't be alphanumeric. 696 | if isAlphaNumeric(l.peek()) { 697 | l.next() 698 | return false 699 | } 700 | return true 701 | } 702 | 703 | // lexQuote scans a quoted string. 704 | func lexQuote(l *lexer) stateFn { 705 | Loop: 706 | for { 707 | switch l.next() { 708 | case '\\': 709 | if r := l.next(); r != eof && r != '\n' { 710 | break 711 | } 712 | fallthrough 713 | case eof, '\n': 714 | return l.errorf("unterminated quoted string") 715 | case '"': 716 | break Loop 717 | } 718 | } 719 | l.emit(itemString) 720 | return lexInsideAction 721 | } 722 | 723 | // lexRawQuote scans a raw quoted string. 724 | func lexRawQuote(l *lexer) stateFn { 725 | Loop: 726 | for { 727 | switch l.next() { 728 | case eof: 729 | return l.errorf("unterminated raw quoted string") 730 | case '`': 731 | break Loop 732 | } 733 | } 734 | l.emit(itemRawString) 735 | return lexInsideAction 736 | } 737 | 738 | // isSpace reports whether r is a space character. 739 | func isSpace(r rune) bool { 740 | return r == ' ' || r == '\t' || r == '\r' || r == '\n' 741 | } 742 | 743 | // isAlphaNumeric reports whether r is an alphabetic, digit, or underscore. 744 | func isAlphaNumeric(r rune) bool { 745 | return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) 746 | } 747 | 748 | // rightTrimLength returns the length of the spaces at the end of the string. 749 | func rightTrimLength(s string) Pos { 750 | return Pos(len(s) - len(strings.TrimRightFunc(s, isSpace))) 751 | } 752 | 753 | // leftTrimLength returns the length of the spaces at the beginning of the string. 754 | func leftTrimLength(s string) Pos { 755 | return Pos(len(s) - len(strings.TrimLeftFunc(s, isSpace))) 756 | } 757 | 758 | // atRightDelim reports whether the lexer is at a right delimiter, possibly preceded by a trim marker. 759 | func (l *lexer) atRightDelim() (delim, trimSpaces bool) { 760 | if strings.HasPrefix(l.input[l.pos:], l.trimRightDelim) { // With trim marker. 761 | return true, true 762 | } 763 | if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { // Without trim marker. 764 | return true, false 765 | } 766 | return false, false 767 | } 768 | -------------------------------------------------------------------------------- /lex_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import "testing" 18 | 19 | func lexerTestCaseCustomLexer(t *testing.T, lexer *lexer, input string, items ...itemType) { 20 | t.Helper() 21 | for i := 0; i < len(items); i++ { 22 | item := lexer.nextItem() 23 | 24 | for item.typ == itemSpace { 25 | item = lexer.nextItem() 26 | } 27 | 28 | if item.typ != items[i] { 29 | t.Errorf("Unexpected token %s on input on %q => %q", item, input, input[item.pos:]) 30 | return 31 | } 32 | } 33 | item := lexer.nextItem() 34 | if item.typ != itemEOF { 35 | t.Errorf("Unexpected token %s, expected EOF", item) 36 | } 37 | } 38 | 39 | func lexerTestCase(t *testing.T, input string, items ...itemType) { 40 | lexer := lex("test.flowRender", input, true) 41 | lexerTestCaseCustomLexer(t, lexer, input, items...) 42 | } 43 | 44 | func lexerTestCaseCustomDelimiters(t *testing.T, leftDelim, rightDelim, input string, items ...itemType) { 45 | lexer := lex("test.customDelimiters", input, false) 46 | lexer.setDelimiters(leftDelim, rightDelim) 47 | lexer.run() 48 | lexerTestCaseCustomLexer(t, lexer, input, items...) 49 | } 50 | 51 | func TestLexer(t *testing.T) { 52 | lexerTestCase(t, `{{}}`, itemLeftDelim, itemRightDelim) 53 | lexerTestCase(t, `{{- -}}`, itemLeftDelim, itemRightDelim) 54 | lexerTestCase(t, ` {{- -}} `, itemLeftDelim, itemRightDelim) 55 | lexerTestCase(t, `{{ line }}`, itemLeftDelim, itemIdentifier, itemRightDelim) 56 | lexerTestCase(t, ` {{- line -}} `, itemLeftDelim, itemIdentifier, itemRightDelim) 57 | lexerTestCase(t, `{{ . }}`, itemLeftDelim, itemIdentifier, itemRightDelim) 58 | lexerTestCase(t, `{{ .Field }}`, itemLeftDelim, itemField, itemRightDelim) 59 | lexerTestCase(t, `{{ "value" }}`, itemLeftDelim, itemString, itemRightDelim) 60 | lexerTestCase(t, `{{ call: value }}`, itemLeftDelim, itemIdentifier, itemColon, itemIdentifier, itemRightDelim) 61 | lexerTestCase(t, `{{.Ex+1}}`, itemLeftDelim, itemField, itemAdd, itemNumber, itemRightDelim) 62 | lexerTestCase(t, `{{.Ex-1}}`, itemLeftDelim, itemField, itemMinus, itemNumber, itemRightDelim) 63 | lexerTestCase(t, `{{.Ex*1}}`, itemLeftDelim, itemField, itemMul, itemNumber, itemRightDelim) 64 | lexerTestCase(t, `{{.Ex/1}}`, itemLeftDelim, itemField, itemDiv, itemNumber, itemRightDelim) 65 | lexerTestCase(t, `{{.Ex%1}}`, itemLeftDelim, itemField, itemMod, itemNumber, itemRightDelim) 66 | lexerTestCase(t, `{{.Ex=1}}`, itemLeftDelim, itemField, itemAssign, itemNumber, itemRightDelim) 67 | lexerTestCase(t, `{{Ex:=1}}`, itemLeftDelim, itemIdentifier, itemAssign, itemNumber, itemRightDelim) 68 | lexerTestCase(t, `{{.Ex!1}}`, itemLeftDelim, itemField, itemNot, itemNumber, itemRightDelim) 69 | lexerTestCase(t, `{{.Ex==1}}`, itemLeftDelim, itemField, itemEquals, itemNumber, itemRightDelim) 70 | lexerTestCase(t, `{{.Ex&&1}}`, itemLeftDelim, itemField, itemAnd, itemNumber, itemRightDelim) 71 | lexerTestCase(t, `{{ _ = foo }}`, itemLeftDelim, itemUnderscore, itemAssign, itemIdentifier, itemRightDelim) 72 | } 73 | 74 | func TestCustomDelimiters(t *testing.T) { 75 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[]]`, itemLeftDelim, itemRightDelim) 76 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ line ]]`, itemLeftDelim, itemIdentifier, itemRightDelim) 77 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ . ]]`, itemLeftDelim, itemIdentifier, itemRightDelim) 78 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ .Field ]]`, itemLeftDelim, itemField, itemRightDelim) 79 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ "value" ]]`, itemLeftDelim, itemString, itemRightDelim) 80 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[ call: value ]]`, itemLeftDelim, itemIdentifier, itemColon, itemIdentifier, itemRightDelim) 81 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex+1]]`, itemLeftDelim, itemField, itemAdd, itemNumber, itemRightDelim) 82 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex-1]]`, itemLeftDelim, itemField, itemMinus, itemNumber, itemRightDelim) 83 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex*1]]`, itemLeftDelim, itemField, itemMul, itemNumber, itemRightDelim) 84 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex/1]]`, itemLeftDelim, itemField, itemDiv, itemNumber, itemRightDelim) 85 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex%1]]`, itemLeftDelim, itemField, itemMod, itemNumber, itemRightDelim) 86 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex=1]]`, itemLeftDelim, itemField, itemAssign, itemNumber, itemRightDelim) 87 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[Ex:=1]]`, itemLeftDelim, itemIdentifier, itemAssign, itemNumber, itemRightDelim) 88 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex!1]]`, itemLeftDelim, itemField, itemNot, itemNumber, itemRightDelim) 89 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex==1]]`, itemLeftDelim, itemField, itemEquals, itemNumber, itemRightDelim) 90 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[.Ex&&1]]`, itemLeftDelim, itemField, itemAnd, itemNumber, itemRightDelim) 91 | } 92 | 93 | func TestLexNegatives(t *testing.T) { 94 | lexerTestCase(t, `{{ -1 }}`, itemLeftDelim, itemNumber, itemRightDelim) 95 | lexerTestCase(t, `{{ 5 + -1 }}`, itemLeftDelim, itemNumber, itemAdd, itemNumber, itemRightDelim) 96 | lexerTestCase(t, `{{ 5 * -1 }}`, itemLeftDelim, itemNumber, itemMul, itemNumber, itemRightDelim) 97 | lexerTestCase(t, `{{ 5 / +1 }}`, itemLeftDelim, itemNumber, itemDiv, itemNumber, itemRightDelim) 98 | lexerTestCase(t, `{{ 5 % -1 }}`, itemLeftDelim, itemNumber, itemMod, itemNumber, itemRightDelim) 99 | lexerTestCase(t, `{{ 5 == -1000 }}`, itemLeftDelim, itemNumber, itemEquals, itemNumber, itemRightDelim) 100 | 101 | } 102 | 103 | func TestLexer_Bug35(t *testing.T) { 104 | lexerTestCase(t, `{{if x>y}}blahblah...{{end}}`, itemLeftDelim, itemIf, itemIdentifier, itemGreat, itemIdentifier, itemRightDelim, itemText, itemLeftDelim, itemEnd, itemRightDelim) 105 | lexerTestCaseCustomDelimiters(t, "[[", "]]", `[[if x>y]]blahblah...[[end]]`, itemLeftDelim, itemIf, itemIdentifier, itemGreat, itemIdentifier, itemRightDelim, itemText, itemLeftDelim, itemEnd, itemRightDelim) 106 | } 107 | -------------------------------------------------------------------------------- /loader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "io/ioutil" 22 | "os" 23 | "path" 24 | "path/filepath" 25 | "sync" 26 | ) 27 | 28 | // Loader is a minimal interface required for loading templates. 29 | // 30 | // Jet will build an absolute path (with slash delimiters) before looking up templates by resolving paths in extends/import/include statements: 31 | // 32 | // - `{{ extends "/bar.jet" }}` will make Jet look up `/bar.jet` in the Loader unchanged, no matter where it occurs (since it's an absolute path) 33 | // - `{{ include("\views\bar.jet") }}` will make Jet look up `/views/bar.jet` in the Loader, no matter where it occurs 34 | // - `{{ import "bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet` 35 | // - `{{ extends "./bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet` 36 | // - `{{ import "../views\bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/views/bar.jet` 37 | // - `{{ include("../bar.jet") }}` in `/views/foo.jet` will result in a lookup of `/bar.jet` 38 | // - `{{ import "../views/../bar.jet" }}` in `/views/foo.jet` will result in a lookup of `/bar.jet` 39 | // 40 | // This means that the same template will always be looked up using the same path. 41 | // 42 | // Jet will also try appending multiple file endings for convenience: `{{ extends "/bar" }}` will lookup `/bar`, `/bar.jet`, 43 | // `/bar.html.jet` and `/bar.jet.html` (in that order). To avoid unneccessary lookups, use the full file name in your templates (so the first lookup 44 | // is always a hit, or override this list of extensions using Set.SetExtensions(). 45 | type Loader interface { 46 | // Exists returns whether or not a template exists under the requested path. 47 | Exists(templatePath string) bool 48 | 49 | // Open returns the template's contents or an error if something went wrong. 50 | // Calls to Open() will always be preceded by a call to Exists() with the same `templatePath`. 51 | // It is the caller's duty to close the template. 52 | Open(templatePath string) (io.ReadCloser, error) 53 | } 54 | 55 | // OSFileSystemLoader implements Loader interface using OS file system (os.File). 56 | type OSFileSystemLoader struct { 57 | dir string 58 | } 59 | 60 | // compile time check that we implement Loader 61 | var _ Loader = (*OSFileSystemLoader)(nil) 62 | 63 | // NewOSFileSystemLoader returns an initialized OSFileSystemLoader. 64 | func NewOSFileSystemLoader(dirPath string) *OSFileSystemLoader { 65 | return &OSFileSystemLoader{ 66 | dir: filepath.FromSlash(dirPath), 67 | } 68 | } 69 | 70 | // Exists returns true if a file is found under the template path after converting it to a file path 71 | // using the OS's path seperator and joining it with the loader's directory path. 72 | func (l *OSFileSystemLoader) Exists(templatePath string) bool { 73 | templatePath = filepath.Join(l.dir, filepath.FromSlash(templatePath)) 74 | stat, err := os.Stat(templatePath) 75 | if err == nil && !stat.IsDir() { 76 | return true 77 | } 78 | return false 79 | } 80 | 81 | // Open returns the result of `os.Open()` on the file located using the same logic as Exists(). 82 | func (l *OSFileSystemLoader) Open(templatePath string) (io.ReadCloser, error) { 83 | return os.Open(filepath.Join(l.dir, filepath.FromSlash(templatePath))) 84 | } 85 | 86 | // InMemLoader is a simple in-memory loader storing template contents in a simple map. 87 | // InMemLoader normalizes paths passed to its methods by converting any input path to a slash-delimited path, 88 | // turning it into an absolute path by prepending a "/" if neccessary, and cleaning it (see path.Clean()). 89 | // It is safe for concurrent use. 90 | type InMemLoader struct { 91 | lock sync.RWMutex 92 | files map[string][]byte 93 | } 94 | 95 | // compile time check that we implement Loader 96 | var _ Loader = (*InMemLoader)(nil) 97 | 98 | // NewInMemLoader return a new InMemLoader. 99 | func NewInMemLoader() *InMemLoader { 100 | return &InMemLoader{ 101 | files: map[string][]byte{}, 102 | } 103 | } 104 | 105 | func (l *InMemLoader) normalize(templatePath string) string { 106 | templatePath = filepath.ToSlash(templatePath) 107 | return path.Join("/", templatePath) 108 | } 109 | 110 | // Open returns a template's contents, or an error if no template was added under this path using Set(). 111 | func (l *InMemLoader) Open(templatePath string) (io.ReadCloser, error) { 112 | templatePath = l.normalize(templatePath) 113 | l.lock.RLock() 114 | defer l.lock.RUnlock() 115 | f, ok := l.files[templatePath] 116 | if !ok { 117 | return nil, fmt.Errorf("%s does not exist", templatePath) 118 | } 119 | 120 | return ioutil.NopCloser(bytes.NewReader(f)), nil 121 | } 122 | 123 | // Exists returns whether or not a template is indexed under this path. 124 | func (l *InMemLoader) Exists(templatePath string) bool { 125 | templatePath = l.normalize(templatePath) 126 | l.lock.RLock() 127 | defer l.lock.RUnlock() 128 | _, ok := l.files[templatePath] 129 | return ok 130 | } 131 | 132 | // Set adds a template to the loader. 133 | func (l *InMemLoader) Set(templatePath, contents string) { 134 | templatePath = l.normalize(templatePath) 135 | l.lock.Lock() 136 | defer l.lock.Unlock() 137 | l.files[templatePath] = []byte(contents) 138 | } 139 | 140 | // Delete removes whatever contents are stored under the given path. 141 | func (l *InMemLoader) Delete(templatePath string) { 142 | templatePath = l.normalize(templatePath) 143 | l.lock.Lock() 144 | defer l.lock.Unlock() 145 | delete(l.files, templatePath) 146 | } 147 | -------------------------------------------------------------------------------- /loaders/embedfs/embedfs_test.go: -------------------------------------------------------------------------------- 1 | package embedfs 2 | 3 | import ( 4 | "embed" 5 | "testing" 6 | 7 | "github.com/CloudyKit/jet/v6" 8 | "github.com/CloudyKit/jet/v6/jettest" 9 | ) 10 | 11 | //go:embed testData/includeIfNotExists/* 12 | var templateFS embed.FS 13 | 14 | func TestEmbedFileSystemResolve(t *testing.T) { 15 | l := NewLoader("testData/includeIfNotExists", templateFS) 16 | 17 | set := jet.NewSet(l) 18 | jettest.RunWithSet(t, set, nil, nil, "existent", "Hi, i exist!!") 19 | jettest.RunWithSet(t, set, nil, nil, "notExistent", "") 20 | jettest.RunWithSet(t, set, nil, nil, "ifIncludeIfExits", "Hi, i exist!!\n Was included!!\n\n\n Was not included!!\n\n") 21 | jettest.RunWithSet(t, set, nil, "World", "wcontext", "Hi, Buddy!\nHi, World!") 22 | } 23 | -------------------------------------------------------------------------------- /loaders/embedfs/loader.go: -------------------------------------------------------------------------------- 1 | package embedfs 2 | 3 | import ( 4 | "embed" 5 | "io" 6 | "io/fs" 7 | "path/filepath" 8 | 9 | "github.com/CloudyKit/jet/v6" 10 | ) 11 | 12 | type embedFileSystemLoader struct { 13 | dir string 14 | fs embed.FS 15 | } 16 | 17 | // NewLoader returns an initialized loader serving the passed embed.FS. 18 | func NewLoader(dirPath string, fs embed.FS) jet.Loader { 19 | return &embedFileSystemLoader{ 20 | dir: filepath.FromSlash(dirPath), 21 | fs: fs, 22 | } 23 | } 24 | 25 | // Open implements Loader.Open() on top of an embed.FS. 26 | func (l *embedFileSystemLoader) Open(name string) (io.ReadCloser, error) { 27 | return l.fs.Open(filepath.Join(l.dir, filepath.FromSlash(name))) 28 | } 29 | 30 | // Exists implements Loader.Exists() on top of an embed.FS by trying to open the file. 31 | func (l *embedFileSystemLoader) Exists(name string) bool { 32 | name = filepath.Join(l.dir, filepath.FromSlash(name)) 33 | stat, err := fs.Stat(l.fs, name) 34 | if err == nil && !stat.IsDir() { 35 | return true 36 | } 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /loaders/embedfs/testData/includeIfNotExists/existent.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists: "exists.jet"}} -------------------------------------------------------------------------------- /loaders/embedfs/testData/includeIfNotExists/exists.jet: -------------------------------------------------------------------------------- 1 | Hi, i exist!! -------------------------------------------------------------------------------- /loaders/embedfs/testData/includeIfNotExists/ifIncludeIfExits.jet: -------------------------------------------------------------------------------- 1 | {{ if includeIfExists("exists.jet") }} 2 | Was included!! 3 | {{ end }} 4 | {{ if includeIfExists("notExists.jet") }} 5 | Was included!! 6 | {{ else }} 7 | Was not included!! 8 | {{ end }} 9 | -------------------------------------------------------------------------------- /loaders/embedfs/testData/includeIfNotExists/notExistent.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists: "notExists.jet"}} -------------------------------------------------------------------------------- /loaders/embedfs/testData/includeIfNotExists/wcontext.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists("wcontext_child","Buddy") }} 2 | {{ includeIfExists("wcontext_child") }} -------------------------------------------------------------------------------- /loaders/embedfs/testData/includeIfNotExists/wcontext_child.jet: -------------------------------------------------------------------------------- 1 | Hi, {{.}}! -------------------------------------------------------------------------------- /loaders/httpfs/httpfs_test.go: -------------------------------------------------------------------------------- 1 | package httpfs 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/CloudyKit/jet/v6" 8 | "github.com/CloudyKit/jet/v6/jettest" 9 | ) 10 | 11 | func TestNilHTTPFileSystem(t *testing.T) { 12 | const fileName = "does-not-exists.jet" 13 | _, err := NewLoader(nil) 14 | if err == nil { 15 | t.Fatal("NewLoader with nil http.FileSystem should have returned an error but didn't.") 16 | } 17 | } 18 | 19 | func TestHTTPFileSystemResolve(t *testing.T) { 20 | l, err := NewLoader(http.Dir("testData/includeIfNotExists")) 21 | if err != nil { 22 | t.Fatalf("unexpected error from NewLoader: %v", err) 23 | } 24 | set := jet.NewSet(l) 25 | jettest.RunWithSet(t, set, nil, nil, "existent", "Hi, i exist!!") 26 | jettest.RunWithSet(t, set, nil, nil, "notExistent", "") 27 | jettest.RunWithSet(t, set, nil, nil, "ifIncludeIfExits", "Hi, i exist!!\n Was included!!\n\n\n Was not included!!\n\n") 28 | jettest.RunWithSet(t, set, nil, "World", "wcontext", "Hi, Buddy!\nHi, World!") 29 | } 30 | -------------------------------------------------------------------------------- /loaders/httpfs/loader.go: -------------------------------------------------------------------------------- 1 | package httpfs 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/CloudyKit/jet/v6" 9 | ) 10 | 11 | type httpFileSystemLoader struct { 12 | fs http.FileSystem 13 | } 14 | 15 | // NewLoader returns an initialized loader serving the passed http.FileSystem. 16 | func NewLoader(fs http.FileSystem) (jet.Loader, error) { 17 | if fs == nil { 18 | return nil, errors.New("httpfs: nil http.Filesystem passed to NewLoader") 19 | } 20 | return &httpFileSystemLoader{fs: fs}, nil 21 | } 22 | 23 | // Open implements Loader.Open() on top of an http.FileSystem. 24 | func (l *httpFileSystemLoader) Open(name string) (io.ReadCloser, error) { 25 | return l.fs.Open(name) 26 | } 27 | 28 | // Exists implements Loader.Exists() on top of an http.FileSystem by trying to open the file. 29 | func (l *httpFileSystemLoader) Exists(name string) bool { 30 | if f, err := l.Open(name); err == nil { 31 | f.Close() 32 | return true 33 | } 34 | return false 35 | } 36 | -------------------------------------------------------------------------------- /loaders/httpfs/testData/includeIfNotExists/existent.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists: "exists.jet"}} -------------------------------------------------------------------------------- /loaders/httpfs/testData/includeIfNotExists/exists.jet: -------------------------------------------------------------------------------- 1 | Hi, i exist!! -------------------------------------------------------------------------------- /loaders/httpfs/testData/includeIfNotExists/ifIncludeIfExits.jet: -------------------------------------------------------------------------------- 1 | {{ if includeIfExists("exists.jet") }} 2 | Was included!! 3 | {{ end }} 4 | {{ if includeIfExists("notExists.jet") }} 5 | Was included!! 6 | {{ else }} 7 | Was not included!! 8 | {{ end }} 9 | -------------------------------------------------------------------------------- /loaders/httpfs/testData/includeIfNotExists/notExistent.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists: "notExists.jet"}} -------------------------------------------------------------------------------- /loaders/httpfs/testData/includeIfNotExists/wcontext.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists("wcontext_child","Buddy") }} 2 | {{ includeIfExists("wcontext_child") }} -------------------------------------------------------------------------------- /loaders/httpfs/testData/includeIfNotExists/wcontext_child.jet: -------------------------------------------------------------------------------- 1 | Hi, {{.}}! -------------------------------------------------------------------------------- /loaders/multi/multi.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/CloudyKit/jet/v6" 8 | ) 9 | 10 | var _ jet.Loader = (*Multi)(nil) 11 | 12 | // Multi implements jet.Loader interface and tries to load templates from a list of custom loaders. 13 | // Caution: When multiple loaders have templates with the same name, the order in which you pass loaders 14 | // to NewLoader/AddLoaders dictates which template will be returned by Open when you request it! 15 | type Multi struct { 16 | loaders []jet.Loader 17 | } 18 | 19 | // NewLoader returns a new multi loader. The order of the loaders passed as parameters 20 | // will define the order in which templates are loaded. 21 | func NewLoader(loaders ...jet.Loader) *Multi { 22 | return &Multi{loaders: loaders} 23 | } 24 | 25 | // AddLoaders adds the passed loaders to the list of loaders. 26 | func (m *Multi) AddLoaders(loaders ...jet.Loader) { 27 | m.loaders = append(m.loaders, loaders...) 28 | } 29 | 30 | // ClearLoaders clears the list of loaders. 31 | func (m *Multi) ClearLoaders() { 32 | m.loaders = nil 33 | } 34 | 35 | // Open will open the file passed by trying all loaders in succession. 36 | func (m *Multi) Open(name string) (io.ReadCloser, error) { 37 | for _, loader := range m.loaders { 38 | if f, err := loader.Open(name); err == nil { 39 | return f, nil 40 | } 41 | } 42 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} 43 | } 44 | 45 | // Exists checks all loaders in succession, returning true if the template file was found or false 46 | // if no loader can provide the file. 47 | func (m *Multi) Exists(name string) bool { 48 | for _, loader := range m.loaders { 49 | if ok := loader.Exists(name); ok { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | -------------------------------------------------------------------------------- /loaders/multi/multi_test.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/CloudyKit/jet/v6" 8 | "github.com/CloudyKit/jet/v6/jettest" 9 | "github.com/CloudyKit/jet/v6/loaders/httpfs" 10 | ) 11 | 12 | func TestZeroLoaders(t *testing.T) { 13 | const fileName = "does-not-exists.jet" 14 | l := NewLoader() 15 | if _, err := l.Open("does-not-exist.jet"); err == nil { 16 | t.Fatal("Open should have returned an error but didn't.") 17 | } 18 | ok := l.Exists(fileName) 19 | if ok { 20 | t.Fatal("Exists called on an empty file system should have returned empty and false but reported true") 21 | } 22 | } 23 | 24 | func TestTwoLoaders(t *testing.T) { 25 | osFSLoader := jet.NewOSFileSystemLoader("./testData") 26 | httpFSLoader, err := httpfs.NewLoader(http.Dir("../../testData")) 27 | if err != nil { 28 | t.Fatalf("unexpected error from httpfs.NewLoader: %v", err) 29 | } 30 | l := NewLoader(osFSLoader, httpFSLoader) 31 | set := jet.NewSet(l) 32 | jettest.RunWithSet(t, set, nil, nil, "resolve/simple.jet", "simple.jet") 33 | jettest.RunWithSet(t, set, nil, nil, "base.jet", "") 34 | jettest.RunWithSet(t, set, nil, nil, "simple2", "simple2\n") 35 | } 36 | -------------------------------------------------------------------------------- /loaders/multi/testData/simple2.jet: -------------------------------------------------------------------------------- 1 | simple2 2 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "path/filepath" 21 | ) 22 | 23 | var textFormat = "%s" //Changed to "%q" in tests for better error messages. 24 | 25 | type Node interface { 26 | Type() NodeType 27 | String() string 28 | Position() Pos 29 | line() int 30 | error(error) 31 | errorf(string, ...interface{}) 32 | } 33 | 34 | type Expression interface { 35 | Node 36 | } 37 | 38 | // Pos represents a byte position in the original input text from which 39 | // this template was parsed. 40 | type Pos int 41 | 42 | func (p Pos) Position() Pos { 43 | return p 44 | } 45 | 46 | // NodeType identifies the type of a parse tree node. 47 | type NodeType int 48 | 49 | type NodeBase struct { 50 | TemplatePath string 51 | Line int 52 | NodeType 53 | Pos 54 | } 55 | 56 | func (node *NodeBase) line() int { 57 | return node.Line 58 | } 59 | 60 | func (node *NodeBase) error(err error) { 61 | node.errorf("%s", err) 62 | } 63 | 64 | func (node *NodeBase) errorf(format string, v ...interface{}) { 65 | panic(fmt.Errorf("Jet Runtime Error (%q:%d): %s", filepath.ToSlash(node.TemplatePath), node.Line, fmt.Sprintf(format, v...))) 66 | } 67 | 68 | // Type returns itself and provides an easy default implementation 69 | // for embedding in a Node. Embedded in all non-trivial Nodes. 70 | func (t NodeType) Type() NodeType { 71 | return t 72 | } 73 | 74 | const ( 75 | NodeText NodeType = iota //Plain text. 76 | NodeAction //A non-control action such as a field evaluation. 77 | NodeChain //A sequence of field accesses. 78 | NodeCommand //An element of a pipeline. 79 | NodeField //A field or method name. 80 | NodeIdentifier //An identifier; always a function name. 81 | NodeUnderscore //An underscore (discard in assignment, or slot in argument list for piped value) 82 | NodeList //A list of Nodes. 83 | NodePipe //A pipeline of commands. 84 | NodeSet 85 | //NodeWith //A with action. 86 | NodeInclude 87 | NodeBlock 88 | nodeEnd //An end action. Not added to tree. 89 | NodeYield 90 | nodeContent 91 | NodeIf //An if action. 92 | nodeElse //An else action. Not added to tree. 93 | NodeRange //A range action. 94 | NodeTry 95 | nodeCatch 96 | NodeReturn 97 | beginExpressions 98 | NodeString //A string constant. 99 | NodeNil //An untyped nil constant. 100 | NodeNumber //A numerical constant. 101 | NodeBool //A boolean constant. 102 | NodeAdditiveExpr 103 | NodeMultiplicativeExpr 104 | NodeComparativeExpr 105 | NodeNumericComparativeExpr 106 | NodeLogicalExpr 107 | NodeCallExpr 108 | NodeNotExpr 109 | NodeTernaryExpr 110 | NodeIndexExpr 111 | NodeSliceExpr 112 | endExpressions 113 | ) 114 | 115 | // Nodes. 116 | 117 | // ListNode holds a sequence of nodes. 118 | type ListNode struct { 119 | NodeBase 120 | Nodes []Node //The element nodes in lexical order. 121 | } 122 | 123 | func (l *ListNode) append(n Node) { 124 | l.Nodes = append(l.Nodes, n) 125 | } 126 | 127 | func (l *ListNode) String() string { 128 | b := new(bytes.Buffer) 129 | for _, n := range l.Nodes { 130 | fmt.Fprint(b, n) 131 | } 132 | return b.String() 133 | } 134 | 135 | // TextNode holds plain text. 136 | type TextNode struct { 137 | NodeBase 138 | Text []byte 139 | } 140 | 141 | func (t *TextNode) String() string { 142 | return fmt.Sprintf(textFormat, t.Text) 143 | } 144 | 145 | // PipeNode holds a pipeline with optional declaration 146 | type PipeNode struct { 147 | NodeBase //The line number in the input. Deprecated: Kept for compatibility. 148 | Cmds []*CommandNode //The commands in lexical order. 149 | } 150 | 151 | func (p *PipeNode) append(command *CommandNode) { 152 | p.Cmds = append(p.Cmds, command) 153 | } 154 | 155 | func (p *PipeNode) String() string { 156 | s := "" 157 | for i, c := range p.Cmds { 158 | if i > 0 { 159 | s += " | " 160 | } 161 | s += c.String() 162 | } 163 | return s 164 | } 165 | 166 | // ActionNode holds an action (something bounded by delimiters). 167 | // Control actions have their own nodes; ActionNode represents simple 168 | // ones such as field evaluations and parenthesized pipelines. 169 | type ActionNode struct { 170 | NodeBase 171 | Set *SetNode 172 | Pipe *PipeNode 173 | } 174 | 175 | func (a *ActionNode) String() string { 176 | if a.Set != nil { 177 | if a.Pipe == nil { 178 | return fmt.Sprintf("{{%s}}", a.Set) 179 | } 180 | return fmt.Sprintf("{{%s;%s}}", a.Set, a.Pipe) 181 | } 182 | return fmt.Sprintf("{{%s}}", a.Pipe) 183 | } 184 | 185 | // CommandNode holds a command (a pipeline inside an evaluating action). 186 | type CommandNode struct { 187 | NodeBase 188 | CallExprNode 189 | } 190 | 191 | func (c *CommandNode) append(arg Node) { 192 | c.Exprs = append(c.Exprs, arg) 193 | } 194 | 195 | func (c *CommandNode) String() string { 196 | if c.Exprs == nil { 197 | return c.BaseExpr.String() 198 | } 199 | 200 | arguments := "" 201 | for i, expr := range c.Exprs { 202 | if i > 0 { 203 | arguments += ", " 204 | } 205 | arguments += expr.String() 206 | } 207 | return fmt.Sprintf("%s(%s)", c.BaseExpr, arguments) 208 | } 209 | 210 | // IdentifierNode holds an identifier. 211 | type IdentifierNode struct { 212 | NodeBase 213 | Ident string //The identifier's name. 214 | } 215 | 216 | func (i *IdentifierNode) String() string { 217 | return i.Ident 218 | } 219 | 220 | // UnderscoreNode is used for one of two things: 221 | // - signals to discard the corresponding right side of an assignment 222 | // - tells Jet where in a pipelined function call to inject the piped value 223 | type UnderscoreNode struct { 224 | NodeBase 225 | } 226 | 227 | func (i *UnderscoreNode) String() string { 228 | return "_" 229 | } 230 | 231 | // NilNode holds the special identifier 'nil' representing an untyped nil constant. 232 | type NilNode struct { 233 | NodeBase 234 | } 235 | 236 | func (n *NilNode) String() string { 237 | return "nil" 238 | } 239 | 240 | // FieldNode holds a field (identifier starting with '.'). 241 | // The names may be chained ('.x.y'). 242 | // The period is dropped from each ident. 243 | type FieldNode struct { 244 | NodeBase 245 | Ident []string //The identifiers in lexical order. 246 | } 247 | 248 | func (f *FieldNode) String() string { 249 | s := "" 250 | for _, id := range f.Ident { 251 | s += "." + id 252 | } 253 | return s 254 | } 255 | 256 | // ChainNode holds a term followed by a chain of field accesses (identifier starting with '.'). 257 | // The names may be chained ('.x.y'). 258 | // The periods are dropped from each ident. 259 | type ChainNode struct { 260 | NodeBase 261 | Node Node 262 | Field []string //The identifiers in lexical order. 263 | } 264 | 265 | // Add adds the named field (which should start with a period) to the end of the chain. 266 | func (c *ChainNode) Add(field string) { 267 | if len(field) == 0 || field[0] != '.' { 268 | panic("no dot in field") 269 | } 270 | field = field[1:] //Remove leading dot. 271 | if field == "" { 272 | panic("empty field") 273 | } 274 | c.Field = append(c.Field, field) 275 | } 276 | 277 | func (c *ChainNode) String() string { 278 | s := c.Node.String() 279 | if _, ok := c.Node.(*PipeNode); ok { 280 | s = "(" + s + ")" 281 | } 282 | for _, field := range c.Field { 283 | s += "." + field 284 | } 285 | return s 286 | } 287 | 288 | // BoolNode holds a boolean constant. 289 | type BoolNode struct { 290 | NodeBase 291 | True bool //The value of the boolean constant. 292 | } 293 | 294 | func (b *BoolNode) String() string { 295 | if b.True { 296 | return "true" 297 | } 298 | return "false" 299 | } 300 | 301 | // NumberNode holds a number: signed or unsigned integer, float, or complex. 302 | // The value is parsed and stored under all the types that can represent the value. 303 | // This simulates in a small amount of code the behavior of Go's ideal constants. 304 | type NumberNode struct { 305 | NodeBase 306 | 307 | IsInt bool //Number has an integral value. 308 | IsUint bool //Number has an unsigned integral value. 309 | IsFloat bool //Number has a floating-point value. 310 | IsComplex bool //Number is complex. 311 | Int64 int64 //The signed integer value. 312 | Uint64 uint64 //The unsigned integer value. 313 | Float64 float64 //The floating-point value. 314 | Complex128 complex128 //The complex value. 315 | Text string //The original textual representation from the input. 316 | } 317 | 318 | // simplifyComplex pulls out any other types that are represented by the complex number. 319 | // These all require that the imaginary part be zero. 320 | func (n *NumberNode) simplifyComplex() { 321 | n.IsFloat = imag(n.Complex128) == 0 322 | if n.IsFloat { 323 | n.Float64 = real(n.Complex128) 324 | n.IsInt = float64(int64(n.Float64)) == n.Float64 325 | if n.IsInt { 326 | n.Int64 = int64(n.Float64) 327 | } 328 | n.IsUint = float64(uint64(n.Float64)) == n.Float64 329 | if n.IsUint { 330 | n.Uint64 = uint64(n.Float64) 331 | } 332 | } 333 | } 334 | 335 | func (n *NumberNode) String() string { 336 | return n.Text 337 | } 338 | 339 | // StringNode holds a string constant. The value has been "unquoted". 340 | type StringNode struct { 341 | NodeBase 342 | 343 | Quoted string //The original text of the string, with quotes. 344 | Text string //The string, after quote processing. 345 | } 346 | 347 | func (s *StringNode) String() string { 348 | return s.Quoted 349 | } 350 | 351 | // endNode represents an {{end}} action. 352 | // It does not appear in the final parse tree. 353 | type endNode struct { 354 | NodeBase 355 | } 356 | 357 | func (e *endNode) String() string { 358 | return "{{end}}" 359 | } 360 | 361 | // endNode represents an {{end}} action. 362 | // It does not appear in the final parse tree. 363 | type contentNode struct { 364 | NodeBase 365 | } 366 | 367 | func (e *contentNode) String() string { 368 | return "{{content}}" 369 | } 370 | 371 | // elseNode represents an {{else}} action. Does not appear in the final tree. 372 | type elseNode struct { 373 | NodeBase //The line number in the input. Deprecated: Kept for compatibility. 374 | } 375 | 376 | func (e *elseNode) String() string { 377 | return "{{else}}" 378 | } 379 | 380 | // SetNode represents a set action, ident( ',' ident)* '=' expression ( ',' expression )* 381 | type SetNode struct { 382 | NodeBase 383 | Let bool 384 | IndexExprGetLookup bool 385 | Left []Expression 386 | Right []Expression 387 | } 388 | 389 | func (set *SetNode) String() string { 390 | var s = "" 391 | 392 | for i, v := range set.Left { 393 | if i > 0 { 394 | s += ", " 395 | } 396 | s += v.String() 397 | } 398 | 399 | if set.Let { 400 | s += ":=" 401 | } else { 402 | s += "=" 403 | } 404 | 405 | for i, v := range set.Right { 406 | if i > 0 { 407 | s += ", " 408 | } 409 | s += v.String() 410 | } 411 | 412 | return s 413 | } 414 | 415 | // BranchNode is the common representation of if, range, and with. 416 | type BranchNode struct { 417 | NodeBase 418 | Set *SetNode 419 | Expression Expression 420 | List *ListNode 421 | ElseList *ListNode 422 | } 423 | 424 | func (b *BranchNode) String() string { 425 | 426 | if b.NodeType == NodeRange { 427 | s := "" 428 | if b.Set != nil { 429 | s = b.Set.String() 430 | } else { 431 | s = b.Expression.String() 432 | } 433 | 434 | if b.ElseList != nil { 435 | return fmt.Sprintf("{{range %s}}%s{{else}}%s{{end}}", s, b.List, b.ElseList) 436 | } 437 | return fmt.Sprintf("{{range %s}}%s{{end}}", s, b.List) 438 | } else { 439 | s := "" 440 | if b.Set != nil { 441 | s = b.Set.String() + ";" 442 | } 443 | if b.ElseList != nil { 444 | return fmt.Sprintf("{{if %s%s}}%s{{else}}%s{{end}}", s, b.Expression, b.List, b.ElseList) 445 | } 446 | return fmt.Sprintf("{{if %s%s}}%s{{end}}", s, b.Expression, b.List) 447 | } 448 | } 449 | 450 | // IfNode represents an {{if}} action and its commands. 451 | type IfNode struct { 452 | BranchNode 453 | } 454 | 455 | // RangeNode represents a {{range}} action and its commands. 456 | type RangeNode struct { 457 | BranchNode 458 | } 459 | 460 | type BlockParameter struct { 461 | Identifier string 462 | Expression Expression 463 | } 464 | 465 | type BlockParameterList struct { 466 | NodeBase 467 | List []BlockParameter 468 | } 469 | 470 | func (bplist *BlockParameterList) Param(name string) (Expression, int) { 471 | for i := 0; i < len(bplist.List); i++ { 472 | param := &bplist.List[i] 473 | if param.Identifier == name { 474 | return param.Expression, i 475 | } 476 | } 477 | return nil, -1 478 | } 479 | 480 | func (bplist *BlockParameterList) String() (str string) { 481 | buff := bytes.NewBuffer(nil) 482 | for _, bp := range bplist.List { 483 | if bp.Identifier == "" { 484 | fmt.Fprintf(buff, "%s,", bp.Expression) 485 | } else { 486 | if bp.Expression == nil { 487 | fmt.Fprintf(buff, "%s,", bp.Identifier) 488 | } else { 489 | fmt.Fprintf(buff, "%s=%s,", bp.Identifier, bp.Expression) 490 | } 491 | } 492 | } 493 | if buff.Len() > 0 { 494 | str = buff.String()[0 : buff.Len()-1] 495 | } 496 | return 497 | } 498 | 499 | // BlockNode represents a {{block }} action. 500 | type BlockNode struct { 501 | NodeBase //The line number in the input. Deprecated: Kept for compatibility. 502 | Name string //The name of the template (unquoted). 503 | 504 | Parameters *BlockParameterList 505 | Expression Expression //The command to evaluate as dot for the template. 506 | 507 | List *ListNode 508 | Content *ListNode 509 | } 510 | 511 | func (t *BlockNode) String() string { 512 | if t.Content != nil { 513 | if t.Expression == nil { 514 | return fmt.Sprintf("{{block %s(%s)}}%s{{content}}%s{{end}}", t.Name, t.Parameters, t.List, t.Content) 515 | } 516 | return fmt.Sprintf("{{block %s(%s) %s}}%s{{content}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.List, t.Content) 517 | } 518 | if t.Expression == nil { 519 | return fmt.Sprintf("{{block %s(%s)}}%s{{end}}", t.Name, t.Parameters, t.List) 520 | } 521 | return fmt.Sprintf("{{block %s(%s) %s}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.List) 522 | } 523 | 524 | // YieldNode represents a {{yield}} action 525 | type YieldNode struct { 526 | NodeBase //The line number in the input. Deprecated: Kept for compatibility. 527 | Name string //The name of the template (unquoted). 528 | Parameters *BlockParameterList 529 | Expression Expression //The command to evaluate as dot for the template. 530 | Content *ListNode 531 | IsContent bool 532 | } 533 | 534 | func (t *YieldNode) String() string { 535 | if t.IsContent { 536 | if t.Expression == nil { 537 | return "{{yield content}}" 538 | } 539 | return fmt.Sprintf("{{yield content %s}}", t.Expression) 540 | } 541 | 542 | if t.Content != nil { 543 | if t.Expression == nil { 544 | return fmt.Sprintf("{{yield %s(%s) content}}%s{{end}}", t.Name, t.Parameters, t.Content) 545 | } 546 | return fmt.Sprintf("{{yield %s(%s) %s content}}%s{{end}}", t.Name, t.Parameters, t.Expression, t.Content) 547 | } 548 | 549 | if t.Expression == nil { 550 | return fmt.Sprintf("{{yield %s(%s)}}", t.Name, t.Parameters) 551 | } 552 | return fmt.Sprintf("{{yield %s(%s) %s}}", t.Name, t.Parameters, t.Expression) 553 | } 554 | 555 | // IncludeNode represents a {{include }} action. 556 | type IncludeNode struct { 557 | NodeBase 558 | Name Expression 559 | Context Expression 560 | } 561 | 562 | func (t *IncludeNode) String() string { 563 | if t.Context == nil { 564 | return fmt.Sprintf("{{include %s}}", t.Name) 565 | } 566 | return fmt.Sprintf("{{include %s %s}}", t.Name, t.Context) 567 | } 568 | 569 | type binaryExprNode struct { 570 | NodeBase 571 | Operator item 572 | Left, Right Expression 573 | } 574 | 575 | func (node *binaryExprNode) String() string { 576 | return fmt.Sprintf("%s %s %s", node.Left, node.Operator.val, node.Right) 577 | } 578 | 579 | // AdditiveExprNode represents an add or subtract expression 580 | // ex: expression ( '+' | '-' ) expression 581 | type AdditiveExprNode struct { 582 | binaryExprNode 583 | } 584 | 585 | // MultiplicativeExprNode represents a multiplication, division, or module expression 586 | // ex: expression ( '*' | '/' | '%' ) expression 587 | type MultiplicativeExprNode struct { 588 | binaryExprNode 589 | } 590 | 591 | // LogicalExprNode represents a boolean expression, 'and' or 'or' 592 | // ex: expression ( '&&' | '||' ) expression 593 | type LogicalExprNode struct { 594 | binaryExprNode 595 | } 596 | 597 | // ComparativeExprNode represents a comparative expression 598 | // ex: expression ( '==' | '!=' ) expression 599 | type ComparativeExprNode struct { 600 | binaryExprNode 601 | } 602 | 603 | // NumericComparativeExprNode represents a numeric comparative expression 604 | // ex: expression ( '<' | '>' | '<=' | '>=' ) expression 605 | type NumericComparativeExprNode struct { 606 | binaryExprNode 607 | } 608 | 609 | // NotExprNode represents a negate expression 610 | // ex: '!' expression 611 | type NotExprNode struct { 612 | NodeBase 613 | Expr Expression 614 | } 615 | 616 | func (s *NotExprNode) String() string { 617 | return fmt.Sprintf("!%s", s.Expr) 618 | } 619 | 620 | type CallArgs struct { 621 | Exprs []Expression 622 | HasPipeSlot bool 623 | } 624 | 625 | // CallExprNode represents a call expression 626 | // ex: expression '(' (expression (',' expression)* )? ')' 627 | type CallExprNode struct { 628 | NodeBase 629 | BaseExpr Expression 630 | CallArgs 631 | } 632 | 633 | func (s *CallExprNode) String() string { 634 | arguments := "" 635 | for i, expr := range s.Exprs { 636 | if i > 0 { 637 | arguments += ", " 638 | } 639 | arguments += expr.String() 640 | } 641 | return fmt.Sprintf("%s(%s)", s.BaseExpr, arguments) 642 | } 643 | 644 | // TernaryExprNod represents a ternary expression, 645 | // ex: expression '?' expression ':' expression 646 | type TernaryExprNode struct { 647 | NodeBase 648 | Boolean, Left, Right Expression 649 | } 650 | 651 | func (s *TernaryExprNode) String() string { 652 | return fmt.Sprintf("%s?%s:%s", s.Boolean, s.Left, s.Right) 653 | } 654 | 655 | type IndexExprNode struct { 656 | NodeBase 657 | Base Expression 658 | Index Expression 659 | } 660 | 661 | func (s *IndexExprNode) String() string { 662 | return fmt.Sprintf("%s[%s]", s.Base, s.Index) 663 | } 664 | 665 | type SliceExprNode struct { 666 | NodeBase 667 | Base Expression 668 | Index Expression 669 | EndIndex Expression 670 | } 671 | 672 | func (s *SliceExprNode) String() string { 673 | var index_string, len_string string 674 | if s.Index != nil { 675 | index_string = s.Index.String() 676 | } 677 | if s.EndIndex != nil { 678 | len_string = s.EndIndex.String() 679 | } 680 | return fmt.Sprintf("%s[%s:%s]", s.Base, index_string, len_string) 681 | } 682 | 683 | type ReturnNode struct { 684 | NodeBase 685 | Value Expression 686 | } 687 | 688 | func (n *ReturnNode) String() string { 689 | return fmt.Sprintf("return %v", n.Value) 690 | } 691 | 692 | type TryNode struct { 693 | NodeBase 694 | List *ListNode 695 | Catch *catchNode 696 | } 697 | 698 | func (n *TryNode) String() string { 699 | if n.Catch != nil { 700 | return fmt.Sprintf("{{try}}%s%s", n.List, n.Catch) 701 | } 702 | return fmt.Sprintf("{{try}}%s{{end}}", n.List) 703 | } 704 | 705 | type catchNode struct { 706 | NodeBase 707 | Err *IdentifierNode 708 | List *ListNode 709 | } 710 | 711 | func (n *catchNode) String() string { 712 | return fmt.Sprintf("{{catch %s}}%s{{end}}", n.Err, n.List) 713 | } 714 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "bytes" 19 | "io/ioutil" 20 | "path" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | var parseSet = NewSet(NewOSFileSystemLoader("./testData"), WithSafeWriter(nil)) 26 | 27 | type ParserTestCase struct { 28 | *testing.T 29 | set *Set 30 | } 31 | 32 | func (t ParserTestCase) ExpectPrintName(name, input, output string) { 33 | set := parseSet 34 | if t.set != nil { 35 | set = t.set 36 | } 37 | template, err := set.parse(name, input, false) 38 | if err != nil { 39 | t.Errorf("%q %s", input, err.Error()) 40 | return 41 | } 42 | expected := strings.Replace(template.String(), "\r\n", "\n", -1) 43 | output = strings.Replace(output, "\r\n", "\n", -1) 44 | if expected != output { 45 | t.Errorf("Unexpected tree on %s Got:\n%s\nExpected: \n%s\n", name, expected, output) 46 | } 47 | } 48 | 49 | func (t ParserTestCase) ExpectPrint(input, output string) { 50 | t.ExpectPrintName("", input, output) 51 | } 52 | 53 | func (t ParserTestCase) ExpectError(name, input, errorMessage string) { 54 | set := parseSet 55 | if t.set != nil { 56 | set = t.set 57 | } 58 | _, err := set.parse(name, input, false) 59 | if err == nil { 60 | t.Errorf("expected %q but got no error", errorMessage) 61 | return 62 | } 63 | if err.Error() != errorMessage { 64 | t.Errorf("expected %q but got %q", errorMessage, err.Error()) 65 | } 66 | } 67 | 68 | func (t ParserTestCase) TestPrintFile(file string) { 69 | content, err := ioutil.ReadFile(path.Join("./testData", file)) 70 | if err != nil { 71 | t.Errorf("file %s not found", file) 72 | return 73 | } 74 | parts := bytes.Split(content, []byte("===")) 75 | t.ExpectPrintName(file, string(bytes.TrimSpace(parts[0])), string(bytes.TrimSpace(parts[1]))) 76 | } 77 | 78 | func (t ParserTestCase) ExpectPrintSame(input string) { 79 | t.ExpectPrint(input, input) 80 | } 81 | 82 | func TestParseTemplateAndImport(t *testing.T) { 83 | p := ParserTestCase{T: t} 84 | p.TestPrintFile("extends.jet") 85 | p.TestPrintFile("imports.jet") 86 | } 87 | 88 | func TestUsefulErrorOnLateImportOrExtends(t *testing.T) { 89 | p := ParserTestCase{T: t} 90 | p.ExpectError("late_import.jet", `{{import "./foo.jet"}}`, "template: late_import.jet:1: parsing command: unexpected keyword 'import' ('import' statements must be at the beginning of the template)") 91 | p.ExpectError("late_extends.jet", `{{extends "./foo.jet"}}`, "template: late_extends.jet:1: parsing command: unexpected keyword 'extends' ('extends' statements must be at the beginning of the template)") 92 | } 93 | 94 | func TestKeywordsDisallowedAsBlockNames(t *testing.T) { 95 | p := ParserTestCase{T: t} 96 | p.ExpectError("block_content.jet", `{{ block content() }}bla{{ end }}`, "template: block_content.jet:1: parsing block clause: unexpected keyword 'content' (expected name)") 97 | p.ExpectError("block_if.jet", `{{ block if() }}bla{{ end }}`, "template: block_if.jet:1: parsing block clause: unexpected keyword 'if' (expected name)") 98 | } 99 | 100 | func TestParseTemplateControl(t *testing.T) { 101 | p := ParserTestCase{T: t} 102 | p.TestPrintFile("if.jet") 103 | p.TestPrintFile("range.jet") 104 | } 105 | 106 | func TestParseTemplateExpressions(t *testing.T) { 107 | p := ParserTestCase{T: t} 108 | p.TestPrintFile("simple_expression.jet") 109 | p.TestPrintFile("additive_expression.jet") 110 | p.TestPrintFile("multiplicative_expression.jet") 111 | } 112 | 113 | func TestParseTemplateBlockYield(t *testing.T) { 114 | p := ParserTestCase{T: t} 115 | p.TestPrintFile("block_yield.jet") 116 | p.TestPrintFile("new_block_yield.jet") 117 | } 118 | 119 | func TestParseTemplateIndexSliceExpression(t *testing.T) { 120 | p := ParserTestCase{T: t} 121 | p.TestPrintFile("index_slice_expression.jet") 122 | } 123 | 124 | func TestParseTemplateAssignment(t *testing.T) { 125 | p := ParserTestCase{T: t} 126 | p.TestPrintFile("assignment.jet") 127 | } 128 | 129 | func TestParseTemplateWithCustomDelimiters(t *testing.T) { 130 | set := NewSet( 131 | NewOSFileSystemLoader("./testData"), 132 | WithSafeWriter(nil), 133 | WithDelims("[[", "]]"), 134 | WithCommentDelims("[*", "*]"), 135 | ) 136 | p := ParserTestCase{T: t, set: set} 137 | p.TestPrintFile("custom_delimiters.jet") 138 | } 139 | -------------------------------------------------------------------------------- /profile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go test -run="^$" -bench="Range" -benchmem -c -cpuprofile=./pprof.out 4 | go test -run="^$" -bench="Range" -benchmem -cpuprofile=./pprof.out 5 | go tool pprof --pdf --focus="$1" jet.test pprof.out >> out.pdf 6 | rm jet.test 7 | rm pprof.out 8 | open out.pdf -------------------------------------------------------------------------------- /ranger.go: -------------------------------------------------------------------------------- 1 | package jet 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "sync" 8 | ) 9 | 10 | // Ranger describes an interface for types that iterate over something. 11 | // Implementing this interface means the ranger will be used when it's 12 | // encountered on the right hand side of a range's "let" expression. 13 | type Ranger interface { 14 | // Range calls should return a key, a value and a done bool to indicate 15 | // whether there are more values to be generated. 16 | // 17 | // When the done flag is true, then the loop ends. 18 | Range() (reflect.Value, reflect.Value, bool) 19 | 20 | // ProvidesIndex should return true if keys are produced during Range() 21 | // calls. This call should be idempotent across Range() calls (i.e. 22 | // its return value must not change during an iteration). 23 | ProvidesIndex() bool 24 | } 25 | 26 | type intsRanger struct { 27 | i, val, to int64 28 | } 29 | 30 | var _ Ranger = &intsRanger{} 31 | 32 | func (r *intsRanger) Range() (index, value reflect.Value, end bool) { 33 | r.i++ 34 | r.val++ 35 | end = r.val == r.to 36 | 37 | // The indirection in the ValueOf calls avoids an allocation versus 38 | // using the concrete value of 'i' and 'val'. The downside is having 39 | // to interpret 'r.i' as "the current value" after Range() returns, 40 | // and so it needs to be initialized as -1. 41 | index = reflect.ValueOf(&r.i).Elem() 42 | value = reflect.ValueOf(&r.val).Elem() 43 | return 44 | } 45 | 46 | func (r *intsRanger) ProvidesIndex() bool { return true } 47 | 48 | func newIntsRanger(from, to int64) *intsRanger { 49 | r := &intsRanger{ 50 | to: to, 51 | i: -1, 52 | val: from - 1, 53 | } 54 | return r 55 | } 56 | 57 | type pooledRanger interface { 58 | Ranger 59 | Setup(reflect.Value) 60 | } 61 | 62 | type sliceRanger struct { 63 | v reflect.Value 64 | i int 65 | } 66 | 67 | var _ Ranger = &sliceRanger{} 68 | var _ pooledRanger = &sliceRanger{} 69 | 70 | func (r *sliceRanger) Setup(v reflect.Value) { 71 | r.i = 0 72 | r.v = v 73 | } 74 | 75 | func (r *sliceRanger) Range() (index, value reflect.Value, end bool) { 76 | if r.i == r.v.Len() { 77 | end = true 78 | return 79 | } 80 | index = reflect.ValueOf(r.i) 81 | value = r.v.Index(r.i) 82 | r.i++ 83 | return 84 | } 85 | 86 | func (r *sliceRanger) ProvidesIndex() bool { return true } 87 | 88 | type mapRanger struct { 89 | iter *reflect.MapIter 90 | hasMore bool 91 | } 92 | 93 | var _ Ranger = &mapRanger{} 94 | var _ pooledRanger = &mapRanger{} 95 | 96 | func (r *mapRanger) Setup(v reflect.Value) { 97 | r.iter = v.MapRange() 98 | r.hasMore = r.iter.Next() 99 | } 100 | 101 | func (r *mapRanger) Range() (key, value reflect.Value, end bool) { 102 | if !r.hasMore { 103 | end = true 104 | return 105 | } 106 | key, value = r.iter.Key(), r.iter.Value() 107 | r.hasMore = r.iter.Next() 108 | return 109 | } 110 | 111 | func (r *mapRanger) ProvidesIndex() bool { return true } 112 | 113 | type chanRanger struct { 114 | v reflect.Value 115 | } 116 | 117 | var _ Ranger = &chanRanger{} 118 | var _ pooledRanger = &chanRanger{} 119 | 120 | func (r *chanRanger) Setup(v reflect.Value) { 121 | r.v = v 122 | } 123 | 124 | func (r *chanRanger) Range() (_, value reflect.Value, end bool) { 125 | v, ok := r.v.Recv() 126 | value, end = v, !ok 127 | return 128 | } 129 | 130 | func (r *chanRanger) ProvidesIndex() bool { return false } 131 | 132 | // ranger pooling 133 | 134 | var ( 135 | poolSliceRanger = &sync.Pool{ 136 | New: func() interface{} { 137 | return new(sliceRanger) 138 | }, 139 | } 140 | 141 | poolsByKind = map[reflect.Kind]*sync.Pool{ 142 | reflect.Slice: poolSliceRanger, 143 | reflect.Array: poolSliceRanger, 144 | reflect.Map: &sync.Pool{ 145 | New: func() interface{} { 146 | return new(mapRanger) 147 | }, 148 | }, 149 | reflect.Chan: &sync.Pool{ 150 | New: func() interface{} { 151 | return new(chanRanger) 152 | }, 153 | }, 154 | } 155 | ) 156 | 157 | func getRanger(v reflect.Value) (r Ranger, cleanup func(), err error) { 158 | if !v.IsValid() { 159 | return nil, nil, errors.New("can't range over invalid value") 160 | } 161 | t := v.Type() 162 | if t.Implements(rangerType) { 163 | return v.Interface().(Ranger), func() { /* no cleanup needed */ }, nil 164 | } 165 | 166 | v, isNil := indirect(v) 167 | if isNil { 168 | return nil, nil, fmt.Errorf("cannot range over nil pointer/interface (%s)", t) 169 | } 170 | 171 | pool, ok := poolsByKind[v.Kind()] 172 | if !ok { 173 | return nil, nil, fmt.Errorf("value %v (type %s) is not rangeable", v, t) 174 | } 175 | 176 | pr := pool.Get().(pooledRanger) 177 | pr.Setup(v) 178 | return pr, func() { pool.Put(pr) }, nil 179 | } 180 | -------------------------------------------------------------------------------- /ranger_test.go: -------------------------------------------------------------------------------- 1 | package jet 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | ) 8 | 9 | // exampleCustomBenchRanger satisfies the Ranger interface, generating fixed 10 | // data. 11 | type exampleCustomRanger struct { 12 | i int 13 | } 14 | 15 | // Type assertion to verify exampleCustomRanger satisfies the Ranger interface. 16 | var _ Ranger = (*exampleCustomRanger)(nil) 17 | 18 | func (ecr *exampleCustomRanger) ProvidesIndex() bool { 19 | // Return false if 'k' can't be filled in Range(). 20 | return true 21 | } 22 | 23 | func (ecr *exampleCustomRanger) Range() (k reflect.Value, v reflect.Value, done bool) { 24 | if ecr.i >= 3 { 25 | done = true 26 | return 27 | } 28 | 29 | k = reflect.ValueOf(ecr.i) 30 | v = reflect.ValueOf(fmt.Sprintf("custom ranger %d", ecr.i)) 31 | ecr.i += 1 32 | return 33 | } 34 | 35 | // ExampleRanger demonstrates how to write a custom template ranger. 36 | func ExampleRanger() { 37 | // Error handling ignored for brevity. 38 | // 39 | // Setup template and rendering. 40 | loader := NewInMemLoader() 41 | loader.Set("template", 42 | `{{ range k := ecr }} 43 | {{k}} = {{.}} 44 | {{- end }} 45 | {{ range k := struct.RangerEface }} 46 | {{k}} = {{.}} 47 | {{- end }}`, 48 | ) 49 | set := NewSet(loader, WithSafeWriter(nil)) 50 | t, _ := set.GetTemplate("template") 51 | 52 | // Pass a custom ranger instance as the 'ecr' var, as well as in a struct field with type interface{}. 53 | vars := VarMap{ 54 | "ecr": reflect.ValueOf(&exampleCustomRanger{}), 55 | "struct": reflect.ValueOf(struct{ RangerEface interface{} }{RangerEface: &exampleCustomRanger{}}), 56 | } 57 | 58 | // Execute template. 59 | _ = t.Execute(os.Stdout, vars, nil) 60 | 61 | // Output: 62 | // 0 = custom ranger 0 63 | // 1 = custom ranger 1 64 | // 2 = custom ranger 2 65 | // 66 | // 0 = custom ranger 0 67 | // 1 = custom ranger 1 68 | // 2 = custom ranger 2 69 | } 70 | -------------------------------------------------------------------------------- /set.go: -------------------------------------------------------------------------------- 1 | package jet 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "path" 8 | "path/filepath" 9 | "reflect" 10 | "sync" 11 | "text/template" 12 | ) 13 | 14 | // Set is responsible to load, parse and cache templates. 15 | // Every Jet template is associated with a Set. 16 | type Set struct { 17 | loader Loader 18 | cache Cache 19 | escapee SafeWriter // escapee to use at runtime 20 | globals VarMap // global scope for this template set 21 | gmx *sync.RWMutex // global variables map mutex 22 | extensions []string 23 | developmentMode bool 24 | leftDelim string 25 | rightDelim string 26 | leftComment string 27 | rightComment string 28 | } 29 | 30 | // Option is the type of option functions that can be used in NewSet(). 31 | type Option func(*Set) 32 | 33 | // NewSet returns a new Set relying on loader. NewSet panics if a nil Loader is passed. 34 | func NewSet(loader Loader, opts ...Option) *Set { 35 | if loader == nil { 36 | panic(errors.New("jet: NewSet() must not be called with a nil loader")) 37 | } 38 | 39 | s := &Set{ 40 | loader: loader, 41 | cache: &cache{}, 42 | escapee: template.HTMLEscape, 43 | globals: VarMap{}, 44 | gmx: &sync.RWMutex{}, 45 | extensions: []string{ 46 | "", // in case the path is given with the correct extension already 47 | ".jet", 48 | ".html.jet", 49 | ".jet.html", 50 | }, 51 | } 52 | 53 | for _, opt := range opts { 54 | opt(s) 55 | } 56 | 57 | return s 58 | } 59 | 60 | // WithCache returns an option function that sets the cache to use for template parsing results. 61 | // Use InDevelopmentMode() to disable caching of parsed templates. By default, Jet uses a 62 | // concurrency-safe in-memory cache that holds templates forever. 63 | func WithCache(c Cache) Option { 64 | if c == nil { 65 | panic(errors.New("jet: WithCache() must not be called with a nil cache")) 66 | } 67 | return func(s *Set) { 68 | s.cache = c 69 | } 70 | } 71 | 72 | // WithSafeWriter returns an option function that sets the escaping function to use when executing 73 | // templates. By default, Jet uses a writer that takes care of HTML escaping. Pass nil to disable escaping. 74 | func WithSafeWriter(w SafeWriter) Option { 75 | return func(s *Set) { 76 | s.escapee = w 77 | } 78 | } 79 | 80 | // WithDelims returns an option function that sets the delimiters to the specified strings. 81 | // Parsed templates will inherit the settings. Not setting them leaves them at the default: `{{` and `}}`. 82 | func WithDelims(left, right string) Option { 83 | return func(s *Set) { 84 | s.leftDelim = left 85 | s.rightDelim = right 86 | } 87 | } 88 | 89 | // WithCommentDelims returns an option function that sets the comment delimiters to the specified strings. 90 | // Parsed templates will inherit the settings. Not setting them leaves them at the default: `{*` and `*}`. 91 | func WithCommentDelims(left, right string) Option { 92 | return func(s *Set) { 93 | s.leftComment = left 94 | s.rightComment = right 95 | } 96 | } 97 | 98 | // WithTemplateNameExtensions returns an option function that sets the extensions to try when looking 99 | // up template names in the cache or loader. Default extensions are `""` (no extension), `".jet"`, 100 | // `".html.jet"`, `".jet.html"`. Extensions will be tried in the order they are defined in the slice. 101 | // WithTemplateNameExtensions panics when you pass in a nil or empty slice. 102 | func WithTemplateNameExtensions(extensions []string) Option { 103 | if len(extensions) == 0 { 104 | panic(errors.New("jet: WithTemplateNameExtensions() must not be called with a nil or empty slice of extensions")) 105 | } 106 | return func(s *Set) { 107 | s.extensions = extensions 108 | } 109 | } 110 | 111 | // InDevelopmentMode returns an option function that toggles development mode on, meaning the cache will 112 | // always be bypassed and every template lookup will go to the loader. 113 | func InDevelopmentMode() Option { 114 | return DevelopmentMode(true) 115 | } 116 | 117 | // DevelopmentMode returns an option function that sets development mode on or off. "On" means the cache will 118 | // always be bypassed and every template lookup will go to the loader. 119 | func DevelopmentMode(mode bool) Option { 120 | return func(s *Set) { 121 | s.developmentMode = mode 122 | } 123 | } 124 | 125 | // GetTemplate tries to find (and parse, if not yet parsed) the template at the specified path. 126 | // 127 | // For example, GetTemplate("catalog/products.list") with extensions set to []string{"", ".html.jet",".jet"} 128 | // will try to look for: 129 | // 1. catalog/products.list 130 | // 2. catalog/products.list.html.jet 131 | // 3. catalog/products.list.jet 132 | // in the set's templates cache, and if it can't find the template it will try to load the same paths via 133 | // the loader, and, if parsed successfully, cache the template (unless running in development mode). 134 | func (s *Set) GetTemplate(templatePath string) (t *Template, err error) { 135 | return s.getSiblingTemplate(templatePath, "/", true) 136 | } 137 | 138 | func (s *Set) getSiblingTemplate(templatePath, siblingPath string, cacheAfterParsing bool) (t *Template, err error) { 139 | templatePath = filepath.ToSlash(templatePath) 140 | siblingPath = filepath.ToSlash(siblingPath) 141 | if !path.IsAbs(templatePath) { 142 | siblingDir := path.Dir(siblingPath) 143 | templatePath = path.Join(siblingDir, templatePath) 144 | } 145 | return s.getTemplate(templatePath, cacheAfterParsing) 146 | } 147 | 148 | // same as GetTemplate, but doesn't cache a template when found through the loader. 149 | func (s *Set) getTemplate(templatePath string, cacheAfterParsing bool) (t *Template, err error) { 150 | if !s.developmentMode { 151 | t, found := s.getTemplateFromCache(templatePath) 152 | if found { 153 | return t, nil 154 | } 155 | } 156 | 157 | t, err = s.getTemplateFromLoader(templatePath, cacheAfterParsing) 158 | if err == nil && cacheAfterParsing && !s.developmentMode { 159 | s.cache.Put(templatePath, t) 160 | } 161 | return t, err 162 | } 163 | 164 | func (s *Set) getTemplateFromCache(templatePath string) (t *Template, ok bool) { 165 | // check path with all possible extensions in cache 166 | for _, extension := range s.extensions { 167 | canonicalPath := templatePath + extension 168 | if t := s.cache.Get(canonicalPath); t != nil { 169 | return t, true 170 | } 171 | } 172 | return nil, false 173 | } 174 | 175 | func (s *Set) getTemplateFromLoader(templatePath string, cacheAfterParsing bool) (t *Template, err error) { 176 | // check path with all possible extensions in loader 177 | for _, extension := range s.extensions { 178 | canonicalPath := templatePath + extension 179 | if found := s.loader.Exists(canonicalPath); found { 180 | return s.loadFromFile(canonicalPath, cacheAfterParsing) 181 | } 182 | } 183 | return nil, fmt.Errorf("template %s could not be found", templatePath) 184 | } 185 | 186 | func (s *Set) loadFromFile(templatePath string, cacheAfterParsing bool) (template *Template, err error) { 187 | f, err := s.loader.Open(templatePath) 188 | if err != nil { 189 | return nil, err 190 | } 191 | defer f.Close() 192 | content, err := ioutil.ReadAll(f) 193 | if err != nil { 194 | return nil, err 195 | } 196 | return s.parse(templatePath, string(content), cacheAfterParsing) 197 | } 198 | 199 | // Parse parses `contents` as if it were located at `templatePath`, but won't put the result into the cache. 200 | // Any referenced template (e.g. via `extends` or `import` statements) will be tried to be loaded from the cache. 201 | // If a referenced template has to be loaded and parsed, it will also not be put into the cache after parsing. 202 | func (s *Set) Parse(templatePath, contents string) (template *Template, err error) { 203 | templatePath = filepath.ToSlash(templatePath) 204 | switch path.Base(templatePath) { 205 | case ".", "/": 206 | return nil, errors.New("template path has no base name") 207 | } 208 | // make sure it's absolute and clean it 209 | templatePath = path.Join("/", templatePath) 210 | 211 | return s.parse(templatePath, contents, false) 212 | } 213 | 214 | // AddGlobal adds a global variable into the Set, 215 | // overriding any value previously set under the specified key. 216 | // It returns the Set it was called on to allow for method chaining. 217 | func (s *Set) AddGlobal(key string, i interface{}) *Set { 218 | s.gmx.Lock() 219 | defer s.gmx.Unlock() 220 | s.globals[key] = reflect.ValueOf(i) 221 | return s 222 | } 223 | 224 | // LookupGlobal returns the global variable previously set under the specified key. 225 | // It returns the nil interface and false if no variable exists under that key. 226 | func (s *Set) LookupGlobal(key string) (val interface{}, found bool) { 227 | s.gmx.RLock() 228 | defer s.gmx.RUnlock() 229 | val, found = s.globals[key] 230 | return 231 | } 232 | 233 | // AddGlobalFunc adds a global function into the Set, 234 | // overriding any function previously set under the specified key. 235 | // It returns the Set it was called on to allow for method chaining. 236 | func (s *Set) AddGlobalFunc(key string, fn Func) *Set { 237 | return s.AddGlobal(key, fn) 238 | } 239 | -------------------------------------------------------------------------------- /set_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 José Santos 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package jet 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "testing" 21 | ) 22 | 23 | func TestSetSetExtensions(t *testing.T) { 24 | tests := [][]string{ 25 | {".html.jet", ".jet"}, 26 | {".tmpl", ".html"}, 27 | } 28 | 29 | for _, extensions := range tests { 30 | set := NewSet(NewInMemLoader(), WithTemplateNameExtensions(extensions)) 31 | if !reflect.DeepEqual(extensions, set.extensions) { 32 | t.Errorf("expected extensions %v, got %v", extensions, set.extensions) 33 | } 34 | } 35 | } 36 | 37 | func TestParseDoesNotCache(t *testing.T) { 38 | loader := NewInMemLoader() 39 | set := NewSet(loader) 40 | _, err := set.Parse("/asd.jet", `{{ foo := "bar" }}{{foo}}`) 41 | if err != nil { 42 | t.Fatalf("parsing template: %v", err) 43 | } 44 | (set.cache).(*cache).m.Range(func(_, _ interface{}) bool { 45 | t.Fatalf("template cache is not empty after Parse()") 46 | return false 47 | }) 48 | 49 | loader.Set("/something_to_extend.jet", "some content to extend") 50 | 51 | _, err = set.Parse("/includes_template.jet", `{{ extends "/something_to_extend.jet" }}, and more content`) 52 | if err != nil { 53 | t.Fatalf("parsing template: %v", err) 54 | } 55 | (set.cache).(*cache).m.Range(func(_, _ interface{}) bool { 56 | t.Fatalf("template cache is not empty after Parse()") 57 | return false 58 | }) 59 | } 60 | 61 | func TestGetTemplateConcurrency(t *testing.T) { 62 | l := NewInMemLoader() 63 | l.Set("foo", "{{if true}}Hi {{ .Name }}!{{end}}") 64 | set := NewSet(l) 65 | 66 | for i := 0; i < 100; i++ { 67 | t.Run(fmt.Sprintf("CC_%d", i), func(t *testing.T) { 68 | t.Parallel() 69 | 70 | _, err := set.GetTemplate("foo") 71 | if err != nil { 72 | t.Errorf("getting template from set: %v", err) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /stress.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash -e 2 | 3 | go test -c 4 | # comment above and uncomment below to enable the race builder 5 | #go test -c -race 6 | PKG=$(basename $(pwd)) 7 | 8 | while true ; do 9 | export GOMAXPROCS=$[ 1 + $[ RANDOM % 128 ]] 10 | ./$PKG.test $@ 2>&1 11 | done -------------------------------------------------------------------------------- /testData/additive_expression.jet: -------------------------------------------------------------------------------- 1 | {{ 1+2+2+2 }} 2 | {{ 1 + -5 }} 3 | === 4 | {{1 + 2 + 2 + 2}} 5 | {{1 + -5}} 6 | -------------------------------------------------------------------------------- /testData/assignment.jet: -------------------------------------------------------------------------------- 1 | {{ newURL := url("","").Method(""); newURL |pipe }} 2 | {{ newName := name; safeHtml: newName, " ", "new name" }} 3 | {{ newName,newValue := name,value }} 4 | {{ value,found := name["key"] }} 5 | {{ _ := foo() }} 6 | {{ _ = foo() }} 7 | {{ _, _ = name["key"] }} 8 | === 9 | {{newURL:=url("", "").Method("");newURL | pipe}} 10 | {{newName:=name;safeHtml(newName, " ", "new name")}} 11 | {{newName, newValue:=name, value}} 12 | {{value, found:=name["key"]}} 13 | {{_:=foo()}} 14 | {{_=foo()}} 15 | {{_, _=name["key"]}} -------------------------------------------------------------------------------- /testData/base.jet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudyKit/jet/37c22f6b1633334c102804a9c989c3ca6f47531f/testData/base.jet -------------------------------------------------------------------------------- /testData/block_yield.jet: -------------------------------------------------------------------------------- 1 | {{ extends "base.jet" }} 2 | {{ block mainMenu() }}{{ end }} 3 | {{ block mainContent() }} 4 | {{ yield mainMenu() pipeValue}} 5 | {{ block subContent() request.Post("Name") }} {{ end }} 6 | {{ include "include.jet" }} 7 | {{ end }} 8 | {{ include "include.jet" }} 9 | === 10 | {{extends "base.jet"}} 11 | {{block mainMenu()}}{{end}} 12 | {{block mainContent()}} 13 | {{yield mainMenu() pipeValue}} 14 | {{block subContent() request.Post("Name")}} {{end}} 15 | {{include "include.jet"}} 16 | {{end}} 17 | {{include "include.jet"}} 18 | -------------------------------------------------------------------------------- /testData/custom_delimiters.jet: -------------------------------------------------------------------------------- 1 | [* comment *][[ . ]] 2 | [[ singleValue ]] 3 | [[ nil ]] 4 | [[ "" ]] 5 | [[ val.Field ]] 6 | [[ url: "" ]] 7 | [[ url: "","" |pipe ]] 8 | [[ url("") |pipe |pipe ]] 9 | [[ url("","").Field |pipe ]] 10 | [[ url("","").Method("") |pipe ]] 11 | === 12 | {{.}} 13 | {{singleValue}} 14 | {{nil}} 15 | {{""}} 16 | {{val.Field}} 17 | {{url("")}} 18 | {{url("", "") | pipe}} 19 | {{url("") | pipe | pipe}} 20 | {{url("", "").Field | pipe}} 21 | {{url("", "").Method("") | pipe}} 22 | -------------------------------------------------------------------------------- /testData/devdump.jet: -------------------------------------------------------------------------------- 1 | 1{{- x1:="a string" }} 2 | 2{{- x2 := 1 }} 3 | 3{{- b1 := true }} 4 | 4{{- b2 := false }} 5 | 5{{- s1 := slice("foo", "bar", "baz", "duq")}} 6 | 6{{- m := map("foo", 123) }} 7 | 7{{- mainMenu := "a variable, not a block!" }} 8 | 8{{ block mainMenu(type="text", label="main") }}inside a block{{ end }} 9 | ------------------------------------- dump without parameters 10 | {{ dump() }} 11 | ------------------------------------- dump with depth of 2 12 | {{ dump(2) }} 13 | ------------------------------------- dump with erroneous use 14 | {{ try }} {{ dump(1,"m") }} {{ catch err }} {{- err.Error() -}} {{ end }} 15 | ------------------------------------- dump named 16 | {{ dump("mainMenu", "m") }} 17 | done 18 | === 19 | 1 20 | 2 21 | 3 22 | 4 23 | 5 24 | 6 25 | 7 26 | 8inside a block 27 | ------------------------------------- dump without parameters 28 | Context: 29 | struct { Name string; Surname string } struct { Name string; Surname string }{Name:"John", Surname:"Doe"} 30 | Variables in current scope: 31 | b1=true 32 | b2=false 33 | m=map[string]interface {}{"foo":123} 34 | mainMenu="a variable, not a block!" 35 | s1=[]interface {}{"foo", "bar", "baz", "duq"} 36 | x1="a string" 37 | x2=1 38 | Blocks: 39 | block mainMenu(type="text",label="main"), from /devdump.jet 40 | 41 | ------------------------------------- dump with depth of 2 42 | Context: 43 | struct { Name string; Surname string } struct { Name string; Surname string }{Name:"John", Surname:"Doe"} 44 | Variables in current scope: 45 | b1=true 46 | b2=false 47 | m=map[string]interface {}{"foo":123} 48 | mainMenu="a variable, not a block!" 49 | s1=[]interface {}{"foo", "bar", "baz", "duq"} 50 | x1="a string" 51 | x2=1 52 | Variables in scope 1 level(s) up: 53 | aSlice=[]string{"sliceMember1", "sliceMember2"} 54 | inputMap=map[string]interface {}{"aMap-10":10} 55 | Blocks: 56 | block mainMenu(type="text",label="main"), from /devdump.jet 57 | 58 | ------------------------------------- dump with erroneous use 59 | dump: expected argument 0 to be a string, but got a float64 60 | ------------------------------------- dump named 61 | mainMenu:="a variable, not a block!" // string 62 | block mainMenu(type="text",label="main"), from /devdump.jet 63 | m:=map[string]interface {}{"foo":123} // map[string]interface {} 64 | 65 | done 66 | -------------------------------------------------------------------------------- /testData/execReturn/in_if.jet: -------------------------------------------------------------------------------- 1 | {{if true}} 2 | {{return "from inside if branch"}} 3 | {{end}} 4 | -------------------------------------------------------------------------------- /testData/execReturn/in_include.jet: -------------------------------------------------------------------------------- 1 | bla bla 2 | {{ include "./included.jet" }} 3 | foo 4 | -------------------------------------------------------------------------------- /testData/execReturn/in_range.jet: -------------------------------------------------------------------------------- 1 | {{ range i, v := .arr }} 2 | {{ i }}: {{ v }} 3 | {{ if v == "foo" }}{{ return "from inside if branch inside range" }}{{ end }} 4 | {{ end }} 5 | -------------------------------------------------------------------------------- /testData/execReturn/included.jet: -------------------------------------------------------------------------------- 1 | ... some content that will be discarded when this template runs inside exec() ... 2 | {{ return "from inside included template" }} 3 | -------------------------------------------------------------------------------- /testData/execReturn/simple.jet: -------------------------------------------------------------------------------- 1 | {{ f := "f" }} 2 | {{ o := "o" }} 3 | ... some content that will be discarded when this template runs inside exec() ... 4 | {{ return f+o+o }} 5 | -------------------------------------------------------------------------------- /testData/execReturn/test_in_if.jet: -------------------------------------------------------------------------------- 1 | {{ exec("./in_if.jet") }} 2 | -------------------------------------------------------------------------------- /testData/execReturn/test_in_include.jet: -------------------------------------------------------------------------------- 1 | {{ exec("./in_include") }} 2 | -------------------------------------------------------------------------------- /testData/execReturn/test_in_range.jet: -------------------------------------------------------------------------------- 1 | {{ exec("./in_range.jet", .) }} 2 | -------------------------------------------------------------------------------- /testData/execReturn/test_simple.jet: -------------------------------------------------------------------------------- 1 | {{ foo := exec("./simple") }}{{ foo }} 2 | -------------------------------------------------------------------------------- /testData/extends.jet: -------------------------------------------------------------------------------- 1 | {{ extends "base.jet" }} 2 | === 3 | {{extends "base.jet"}} -------------------------------------------------------------------------------- /testData/if.jet: -------------------------------------------------------------------------------- 1 | {{ if coditionEe }}{{ end }} 2 | {{ if coditionEe.Field }}{{ end }} 3 | {{ if coditionEe.Method(value) }}{{ end }} 4 | {{ if coditionEe.Method(value) }} {{ else }} {{ end }} 5 | {{ if coditionEe.Method(value) }} {{ else if conditionE }} {{ end }} 6 | {{ if isOk=coditionEe.Method(value); isOk }} {{ else if conditionE }} {{ end }} 7 | {{ if isOk=coditionEe.Method(value); isOk }} {{ else if conditionE }} {{ end }} 8 | === 9 | {{if coditionEe}}{{end}} 10 | {{if coditionEe.Field}}{{end}} 11 | {{if coditionEe.Method(value)}}{{end}} 12 | {{if coditionEe.Method(value)}} {{else}} {{end}} 13 | {{if coditionEe.Method(value)}} {{else}}{{if conditionE}} {{end}}{{end}} 14 | {{if isOk=coditionEe.Method(value);isOk}} {{else}}{{if conditionE}} {{end}}{{end}} 15 | {{if isOk=coditionEe.Method(value);isOk}} {{else}}{{if conditionE}} {{end}}{{end}} -------------------------------------------------------------------------------- /testData/imports.jet: -------------------------------------------------------------------------------- 1 | {{ extends "base.jet" }} 2 | {{ import "library.jet" }} 3 | {{ import "library.jet" }} 4 | === 5 | {{extends "base.jet"}} 6 | {{import "library.jet"}} 7 | {{import "library.jet"}} 8 | -------------------------------------------------------------------------------- /testData/includeIfNotExists/broken.jet: -------------------------------------------------------------------------------- 1 | {{ err.break }} -------------------------------------------------------------------------------- /testData/includeIfNotExists/existent.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists: "exists.jet"}} -------------------------------------------------------------------------------- /testData/includeIfNotExists/exists.jet: -------------------------------------------------------------------------------- 1 | Hi, i exist!! -------------------------------------------------------------------------------- /testData/includeIfNotExists/ifIncludeIfExits.jet: -------------------------------------------------------------------------------- 1 | {{ if includeIfExists("exists.jet") }} 2 | Was included!! 3 | {{ end }} 4 | {{ if includeIfExists("notExists.jet") }} 5 | Was included!! 6 | {{ else }} 7 | Was not included!! 8 | {{ end }} 9 | -------------------------------------------------------------------------------- /testData/includeIfNotExists/includeBroken.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists: "broken.jet"}} -------------------------------------------------------------------------------- /testData/includeIfNotExists/notExistent.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists: "notExists.jet"}} -------------------------------------------------------------------------------- /testData/includeIfNotExists/wcontext.jet: -------------------------------------------------------------------------------- 1 | {{ includeIfExists("wcontext_child","Buddy") }} 2 | {{ includeIfExists("wcontext_child") }} -------------------------------------------------------------------------------- /testData/includeIfNotExists/wcontext_child.jet: -------------------------------------------------------------------------------- 1 | Hi, {{.}}! -------------------------------------------------------------------------------- /testData/index_slice_expression.jet: -------------------------------------------------------------------------------- 1 | {{.[1]}} 2 | {{users[1]}} 3 | {{users[1].Func()}} 4 | {{users[1].Contacts[1]}} 5 | {{range .[0:10]}} 6 | {{.Description[:50]}} 7 | {{range .Contacts[1:]}} 8 | {{end}} 9 | {{end}} 10 | === 11 | {{.[1]}} 12 | {{users[1]}} 13 | {{users[1].Func()}} 14 | {{users[1].Contacts[1]}} 15 | {{range .[0:10]}} 16 | {{.Description[:50]}} 17 | {{range .Contacts[1:]}} 18 | {{end}} 19 | {{end}} -------------------------------------------------------------------------------- /testData/library.jet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudyKit/jet/37c22f6b1633334c102804a9c989c3ca6f47531f/testData/library.jet -------------------------------------------------------------------------------- /testData/multiplicative_expression.jet: -------------------------------------------------------------------------------- 1 | {{ 1+2*2+2 }} 2 | {{ 1+2/2+2 }} 3 | {{ 1+2%4 }} 4 | === 5 | {{1 + 2 * 2 + 2}} 6 | {{1 + 2 / 2 + 2}} 7 | {{1 + 2 % 4}} -------------------------------------------------------------------------------- /testData/new_block_yield.jet: -------------------------------------------------------------------------------- 1 | {{ extends "base.jet" }} 2 | {{ block textfield(label,name,value) }} 3 | {{label}}: 4 | {{ end }} 5 | 6 | {{ block col( 7 | md=12, 8 | offset=0, 9 | additionalClass="" 10 | ) }} 11 |
    {{ yield content }}
    12 | {{ end }} 13 | 14 | {{ block row() .}} 15 |
    {{ yield content }}
    16 | {{ content }} 17 |
    18 |
    19 |
    20 | {{ end }} 21 | 22 | {{ block header() }} 23 | {{ yield row() content}} 24 | {{ yield col(md=6, 25 | additionalClass="center" 26 | ) content }} 27 | {{ yield content }} 28 | {{end}} 29 | {{end}} 30 | {{ end }} 31 | 32 | {{ include "include.jet" }} 33 | 34 | === 35 | {{extends "base.jet"}} 36 | {{block textfield(label,name,value)}} 37 | {{label}}: 38 | {{end}} 39 | 40 | {{block col(md=12,offset=0,additionalClass="")}} 41 |
    {{yield content}}
    42 | {{end}} 43 | 44 | {{block row() .}} 45 |
    {{yield content}}
    46 | {{content}} 47 |
    48 |
    49 |
    50 | {{end}} 51 | 52 | {{block header()}} 53 | {{yield row() content}} 54 | {{yield col(md=6,additionalClass="center") content}} 55 | {{yield content}} 56 | {{end}} 57 | {{end}} 58 | {{end}} 59 | 60 | {{include "include.jet"}} 61 | -------------------------------------------------------------------------------- /testData/range.jet: -------------------------------------------------------------------------------- 1 | {{ range coditionEe }}{{ end }} 2 | {{ range coditionEe.Field }}{{ end }} 3 | {{ range coditionEe.Method( 4 | value 5 | ) }}{{ end }} 6 | {{ range coditionEe.Method(value) }} {{ else }} {{ end }} 7 | {{ range index,value = coditionEe.Method(value) }} {{ else }} {{ end }} 8 | {{ range value := coditionEe.Method(value) }} {{ else }} {{ end }} 9 | {{ range value = coditionEe.Method(value) }} {{ else }} {{ end }} 10 | === 11 | {{range coditionEe}}{{end}} 12 | {{range coditionEe.Field}}{{end}} 13 | {{range coditionEe.Method(value)}}{{end}} 14 | {{range coditionEe.Method(value)}} {{else}} {{end}} 15 | {{range index, value=coditionEe.Method(value)}} {{else}} {{end}} 16 | {{range value:=coditionEe.Method(value)}} {{else}} {{end}} 17 | {{range value=coditionEe.Method(value)}} {{else}} {{end}} 18 | -------------------------------------------------------------------------------- /testData/resolve/extension.jet.html: -------------------------------------------------------------------------------- 1 | extension.jet.html -------------------------------------------------------------------------------- /testData/resolve/simple: -------------------------------------------------------------------------------- 1 | simple -------------------------------------------------------------------------------- /testData/resolve/simple.jet: -------------------------------------------------------------------------------- 1 | simple.jet -------------------------------------------------------------------------------- /testData/resolve/sub/extend: -------------------------------------------------------------------------------- 1 | {{extends "subextend"}} -------------------------------------------------------------------------------- /testData/resolve/sub/subextend: -------------------------------------------------------------------------------- 1 | {{include "../simple"}} - {{include "../simple.jet"}} - {{include "../extension"}} -------------------------------------------------------------------------------- /testData/simple_expression.jet: -------------------------------------------------------------------------------- 1 | {{ . }} 2 | {{ singleValue }} 3 | {{ nil }} 4 | {{ "" }} 5 | {{ val.Field }} 6 | {{ url: "" }} 7 | {{ url: "","" |pipe }} 8 | {{ "foo"|pipe (asd) }} 9 | {{ url("") |pipe |pipe }} 10 | {{ url("","").Field |pipe }} 11 | {{ url("","").Method("") |pipe }} 12 | {{ "" }} {{ . }} {{ nil }} 13 | {{ "" -}} {{ . }} {{- nil}} 14 | === 15 | {{.}} 16 | {{singleValue}} 17 | {{nil}} 18 | {{""}} 19 | {{val.Field}} 20 | {{url("")}} 21 | {{url("", "") | pipe}} 22 | {{"foo" | pipe(asd)}} 23 | {{url("") | pipe | pipe}} 24 | {{url("", "").Field | pipe}} 25 | {{url("", "").Method("") | pipe}} 26 | {{""}} {{.}} {{nil}} 27 | {{""}}{{.}}{{nil}} -------------------------------------------------------------------------------- /testData/tryCatch/panic.jet: -------------------------------------------------------------------------------- 1 | some content that won't appear in the output, because the next line will panic ... 2 | {{ undefined_identifier_that_causes_panic }} -------------------------------------------------------------------------------- /testData/tryCatch/try.jet: -------------------------------------------------------------------------------- 1 | before try without panic ... 2 | {{try}} 3 | some content 4 | {{foo := "foo"}} 5 | {{foo}} 6 | {{end}} 7 | after try without panic ... 8 | before panic ... 9 | {{try}} 10 | some content that will not be rendered because the next line panics ... 11 | {{undefined_identifier_that_causes_panic}} 12 | {{end}} 13 | after panic ... -------------------------------------------------------------------------------- /testData/tryCatch/try_catch.jet: -------------------------------------------------------------------------------- 1 | before panic ... 2 | {{try}} 3 | {{undefined_identifier_that_causes_panic}} 4 | {{catch}} 5 | an error occured! 6 | {{end}} 7 | after panic ... -------------------------------------------------------------------------------- /testData/tryCatch/try_catch_err.jet: -------------------------------------------------------------------------------- 1 | before panic ... 2 | {{try}} 3 | {{undefined_identifier_that_causes_panic}} 4 | {{catch err}} 5 | an error occured: {{err}} 6 | {{end}} 7 | after panic ... -------------------------------------------------------------------------------- /testData/tryCatch/try_include.jet: -------------------------------------------------------------------------------- 1 | before broken include ... 2 | {{try}} 3 | {{include "panic.jet"}} 4 | {{end}} 5 | after broken include ... -------------------------------------------------------------------------------- /testData/whitespaceControl/multiple.jet: -------------------------------------------------------------------------------- 1 | before 2 | 3 | {{- "ACTION" -}} 4 | 5 | {{- asd := "produces no output" -}} 6 | 7 | 8 | after -------------------------------------------------------------------------------- /testData/whitespaceControl/simple.jet: -------------------------------------------------------------------------------- 1 | before 2 | 3 | {{- "ACTION" -}} 4 | 5 | after -------------------------------------------------------------------------------- /utils/visitor.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/CloudyKit/jet/v6" 7 | ) 8 | 9 | // Walk walks the template ast and calls the Visit method on each node of the tree 10 | // if you're not familiar with the Visitor pattern please check the visitor_test.go 11 | // for usage examples 12 | func Walk(t *jet.Template, v Visitor) { 13 | v.Visit(VisitorContext{Visitor: v}, t.Root) 14 | } 15 | 16 | // Visitor type implementing the visitor pattern 17 | type Visitor interface { 18 | Visit(vc VisitorContext, node jet.Node) 19 | } 20 | 21 | // VisitorFunc a func that implements the Visitor interface 22 | type VisitorFunc func(vc VisitorContext, node jet.Node) 23 | 24 | func (visitor VisitorFunc) Visit(vc VisitorContext, node jet.Node) { 25 | visitor(vc, node) 26 | } 27 | 28 | // VisitorContext context for the current inspection 29 | type VisitorContext struct { 30 | Visitor Visitor 31 | } 32 | 33 | func (vc VisitorContext) visitNode(node jet.Node) { 34 | vc.Visitor.Visit(vc, node) 35 | } 36 | 37 | func (vc VisitorContext) Visit(node jet.Node) { 38 | 39 | switch node := node.(type) { 40 | case *jet.ListNode: 41 | vc.visitListNode(node) 42 | case *jet.ActionNode: 43 | vc.visitActionNode(node) 44 | case *jet.ChainNode: 45 | vc.visitChainNode(node) 46 | case *jet.CommandNode: 47 | vc.visitCommandNode(node) 48 | case *jet.IfNode: 49 | vc.visitIfNode(node) 50 | case *jet.PipeNode: 51 | vc.visitPipeNode(node) 52 | case *jet.RangeNode: 53 | vc.visitRangeNode(node) 54 | case *jet.BlockNode: 55 | vc.visitBlockNode(node) 56 | case *jet.IncludeNode: 57 | vc.visitIncludeNode(node) 58 | case *jet.YieldNode: 59 | vc.visitYieldNode(node) 60 | case *jet.SetNode: 61 | vc.visitSetNode(node) 62 | case *jet.AdditiveExprNode: 63 | vc.visitAdditiveExprNode(node) 64 | case *jet.MultiplicativeExprNode: 65 | vc.visitMultiplicativeExprNode(node) 66 | case *jet.ComparativeExprNode: 67 | vc.visitComparativeExprNode(node) 68 | case *jet.NumericComparativeExprNode: 69 | vc.visitNumericComparativeExprNode(node) 70 | case *jet.LogicalExprNode: 71 | vc.visitLogicalExprNode(node) 72 | case *jet.CallExprNode: 73 | vc.visitCallExprNode(node) 74 | case *jet.NotExprNode: 75 | vc.visitNotExprNode(node) 76 | case *jet.TernaryExprNode: 77 | vc.visitTernaryExprNode(node) 78 | case *jet.IndexExprNode: 79 | vc.visitIndexExprNode(node) 80 | case *jet.SliceExprNode: 81 | vc.visitSliceExprNode(node) 82 | case *jet.TextNode: 83 | case *jet.IdentifierNode: 84 | case *jet.StringNode: 85 | case *jet.NilNode: 86 | case *jet.NumberNode: 87 | case *jet.BoolNode: 88 | case *jet.FieldNode: 89 | 90 | default: 91 | panic(fmt.Errorf("unexpected node %v", node)) 92 | } 93 | } 94 | 95 | func (vc VisitorContext) visitIncludeNode(includeNode *jet.IncludeNode) { 96 | vc.visitNode(includeNode) 97 | } 98 | 99 | func (vc VisitorContext) visitBlockNode(blockNode *jet.BlockNode) { 100 | 101 | for _, node := range blockNode.Parameters.List { 102 | if node.Expression != nil { 103 | vc.visitNode(node.Expression) 104 | } 105 | } 106 | 107 | if blockNode.Expression != nil { 108 | vc.visitNode(blockNode.Expression) 109 | } 110 | 111 | vc.visitListNode(blockNode.List) 112 | 113 | if blockNode.Content != nil { 114 | vc.visitNode(blockNode.Content) 115 | } 116 | } 117 | 118 | func (vc VisitorContext) visitRangeNode(rangeNode *jet.RangeNode) { 119 | vc.visitBranchNode(&rangeNode.BranchNode) 120 | } 121 | 122 | func (vc VisitorContext) visitPipeNode(pipeNode *jet.PipeNode) { 123 | for _, node := range pipeNode.Cmds { 124 | vc.visitNode(node) 125 | } 126 | } 127 | 128 | func (vc VisitorContext) visitIfNode(ifNode *jet.IfNode) { 129 | vc.visitBranchNode(&ifNode.BranchNode) 130 | } 131 | func (vc VisitorContext) visitBranchNode(branchNode *jet.BranchNode) { 132 | if branchNode.Set != nil { 133 | vc.visitNode(branchNode.Set) 134 | } 135 | 136 | if branchNode.Expression != nil { 137 | vc.visitNode(branchNode.Expression) 138 | } 139 | 140 | vc.visitNode(branchNode.List) 141 | if branchNode.ElseList != nil { 142 | vc.visitNode(branchNode.ElseList) 143 | } 144 | } 145 | 146 | func (vc VisitorContext) visitYieldNode(yieldNode *jet.YieldNode) { 147 | for _, node := range yieldNode.Parameters.List { 148 | if node.Expression != nil { 149 | vc.visitNode(node.Expression) 150 | } 151 | } 152 | if yieldNode.Expression != nil { 153 | vc.visitNode(yieldNode.Expression) 154 | } 155 | if yieldNode.Content != nil { 156 | vc.visitNode(yieldNode.Content) 157 | } 158 | } 159 | 160 | func (vc VisitorContext) visitSetNode(setNode *jet.SetNode) { 161 | for _, node := range setNode.Left { 162 | vc.visitNode(node) 163 | } 164 | for _, node := range setNode.Right { 165 | vc.visitNode(node) 166 | } 167 | } 168 | 169 | func (vc VisitorContext) visitAdditiveExprNode(additiveExprNode *jet.AdditiveExprNode) { 170 | vc.visitNode(additiveExprNode.Left) 171 | vc.visitNode(additiveExprNode.Right) 172 | } 173 | 174 | func (vc VisitorContext) visitMultiplicativeExprNode(multiplicativeExprNode *jet.MultiplicativeExprNode) { 175 | vc.visitNode(multiplicativeExprNode.Left) 176 | vc.visitNode(multiplicativeExprNode.Right) 177 | } 178 | 179 | func (vc VisitorContext) visitComparativeExprNode(comparativeExprNode *jet.ComparativeExprNode) { 180 | vc.visitNode(comparativeExprNode.Left) 181 | vc.visitNode(comparativeExprNode.Right) 182 | } 183 | 184 | func (vc VisitorContext) visitNumericComparativeExprNode(numericComparativeExprNode *jet.NumericComparativeExprNode) { 185 | vc.visitNode(numericComparativeExprNode.Left) 186 | vc.visitNode(numericComparativeExprNode.Right) 187 | } 188 | 189 | func (vc VisitorContext) visitLogicalExprNode(logicalExprNode *jet.LogicalExprNode) { 190 | vc.visitNode(logicalExprNode.Left) 191 | vc.visitNode(logicalExprNode.Right) 192 | } 193 | 194 | func (vc VisitorContext) visitCallExprNode(callExprNode *jet.CallExprNode) { 195 | vc.visitNode(callExprNode.BaseExpr) 196 | for _, node := range callExprNode.Exprs { 197 | vc.visitNode(node) 198 | } 199 | } 200 | 201 | func (vc VisitorContext) visitNotExprNode(notExprNode *jet.NotExprNode) { 202 | vc.visitNode(notExprNode.Expr) 203 | } 204 | 205 | func (vc VisitorContext) visitTernaryExprNode(ternaryExprNode *jet.TernaryExprNode) { 206 | vc.visitNode(ternaryExprNode.Boolean) 207 | vc.visitNode(ternaryExprNode.Left) 208 | vc.visitNode(ternaryExprNode.Right) 209 | } 210 | 211 | func (vc VisitorContext) visitIndexExprNode(indexNode *jet.IndexExprNode) { 212 | vc.visitNode(indexNode.Base) 213 | vc.visitNode(indexNode.Index) 214 | } 215 | 216 | func (vc VisitorContext) visitSliceExprNode(sliceExprNode *jet.SliceExprNode) { 217 | vc.visitNode(sliceExprNode.Base) 218 | vc.visitNode(sliceExprNode.Index) 219 | vc.visitNode(sliceExprNode.EndIndex) 220 | } 221 | 222 | func (vc VisitorContext) visitCommandNode(commandNode *jet.CommandNode) { 223 | vc.visitNode(commandNode.BaseExpr) 224 | for _, node := range commandNode.Exprs { 225 | vc.visitNode(node) 226 | } 227 | } 228 | 229 | func (vc VisitorContext) visitChainNode(chainNode *jet.ChainNode) { 230 | vc.visitNode(chainNode.Node) 231 | } 232 | 233 | func (vc VisitorContext) visitActionNode(actionNode *jet.ActionNode) { 234 | if actionNode.Set != nil { 235 | vc.visitNode(actionNode.Set) 236 | } 237 | if actionNode.Pipe != nil { 238 | vc.visitNode(actionNode.Pipe) 239 | } 240 | } 241 | 242 | func (vc VisitorContext) visitListNode(listNode *jet.ListNode) { 243 | for _, node := range listNode.Nodes { 244 | vc.visitNode(node) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /utils/visitor_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/CloudyKit/jet/v6" 8 | ) 9 | 10 | var ( 11 | Loader = jet.NewInMemLoader() 12 | Set = jet.NewSet(Loader) 13 | ) 14 | 15 | func TestVisitor(t *testing.T) { 16 | var collectedIdentifiers []string 17 | Loader.Set("_testing", "{{ ident1 }}\n{{ ident2(ident3)}}\n{{ if ident4 }}\n {{ident5}}\n{{else}}\n {{ident6}}\n{{end}}\n{{ ident7|ident8|ident9+ident10|ident11[ident12]: ident13[ident14:ident15] }}") 18 | mTemplate, _ := Set.GetTemplate("_testing") 19 | Walk(mTemplate, VisitorFunc(func(context VisitorContext, node jet.Node) { 20 | if node.Type() == jet.NodeIdentifier { 21 | collectedIdentifiers = append(collectedIdentifiers, node.String()) 22 | } 23 | context.Visit(node) 24 | })) 25 | if !reflect.DeepEqual(collectedIdentifiers, []string{"ident1", "ident2", "ident3", "ident4", "ident5", "ident6", "ident7", "ident8", "ident9", "ident10", "ident11", "ident12", "ident13", "ident14", "ident15"}) { 26 | t.Errorf("%q", collectedIdentifiers) 27 | } 28 | } 29 | 30 | func TestSimpleTemplate(t *testing.T) { 31 | Loader.Set("_testing2", "Thank you!\n\n\n\tHello {{userName}},\n\n\tThanks for the order!\n\n\t{{range product := products}}\n\t\t{{product.name}}\n\n\t {{block productPrice(price=product.Price) product}}\n {{if price > ExpensiveProduct}}\n Expensive!!\n {{end}}\n {{end}}\n\n\t\t${{product.price / 100}}\n\t{{end}}\n\n") 32 | mTemplate, err := Set.GetTemplate("_testing2") 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | 37 | var ( 38 | localVariables []string 39 | externalVariables []string 40 | ) 41 | 42 | Walk(mTemplate, VisitorFunc(func(context VisitorContext, node jet.Node) { 43 | 44 | var stackState = len(localVariables) // saves the state of the local identifiers map 45 | 46 | switch node := node.(type) { 47 | case *jet.SetNode: 48 | if node.Let { // check if this is setting a new variable in the current scope 49 | for _, ident := range node.Left { 50 | // push local identifier 51 | localVariables = append(localVariables, ident.String()) 52 | } 53 | } 54 | // continue checking nodes down the tree 55 | context.Visit(node) 56 | case *jet.IdentifierNode: 57 | 58 | // skip local identifiers 59 | for _, varName := range localVariables { 60 | if varName == node.Ident { 61 | return 62 | } 63 | } 64 | 65 | // skip already inserted identifiers 66 | for _, varName := range externalVariables { 67 | if varName == node.Ident { 68 | return 69 | } 70 | } 71 | 72 | // push external identifier 73 | externalVariables = append(externalVariables, node.Ident) 74 | case *jet.ActionNode: 75 | // continue without restore state of local identifiers map 76 | context.Visit(node) 77 | case *jet.BlockNode: 78 | 79 | // iterate over block parameters 80 | for _, param := range node.Parameters.List { 81 | // store block parameters in the local map 82 | localVariables = append(localVariables, param.Identifier) 83 | } 84 | 85 | // continue down tree 86 | context.Visit(node) 87 | // restore local identifiers map 88 | localVariables = localVariables[0:stackState] 89 | default: 90 | // continue down tree 91 | context.Visit(node) 92 | // restore local identifiers map 93 | localVariables = localVariables[0:stackState] 94 | } 95 | 96 | })) 97 | 98 | if !reflect.DeepEqual(externalVariables, []string{"userName", "products", "ExpensiveProduct"}) { 99 | t.Errorf("%q", externalVariables) 100 | } 101 | } 102 | --------------------------------------------------------------------------------