├── .travis.yml ├── LICENSE.md ├── README.md ├── README_CN.md ├── bufferpool.go ├── bufferpool_test.go ├── doc.go ├── examples └── app │ ├── app.go │ └── template │ ├── index.html │ ├── index.html.go │ ├── user.html │ ├── user.html.go │ ├── userlist.html │ ├── userlist.html.go │ ├── userlistwriter.html │ └── userlistwriter.html.go ├── generator.go ├── generator_test.go ├── hero └── main.go ├── parser.go ├── parser_test.go ├── sort.go ├── sort_test.go ├── util.go └── util_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.x 4 | - 1.6 5 | - 1.7.x 6 | - 1.8.x 7 | - master 8 | 9 | install: 10 | go get golang.org/x/tools/cmd/goimports 11 | 12 | script: 13 | go test 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hero 2 | 3 | Hero is a handy, fast and powerful go template engine, which pre-compiles the html templates to go code. 4 | It has been used in production environment in [bthub.io](http://bthub.io). 5 | 6 | [![GoDoc](https://godoc.org/github.com/shiyanhui/hero?status.svg)](https://godoc.org/github.com/shiyanhui/hero) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/shiyanhui/hero)](https://goreportcard.com/report/github.com/shiyanhui/hero) 8 | [![Travis CI](https://travis-ci.org/shiyanhui/hero.svg?branch=master)](https://travis-ci.org/shiyanhui/hero.svg?branch=master) 9 | 10 | [中文文档](https://github.com/shiyanhui/hero/blob/master/README_CN.md) 11 | 12 | - [Features](#features) 13 | - [Install](#install) 14 | - [Usage](#usage) 15 | - [Quick Start](#quick-start) 16 | - [Template Syntax](#template-syntax) 17 | - [License](#license) 18 | 19 | ## Features 20 | 21 | - High performance. 22 | - Easy to use. 23 | - Powerful. template `Extend` and `Include` supported. 24 | - Auto compiling when files change. 25 | 26 | ## Performance 27 | 28 | Hero is the fastest and least-memory used among currently known template engines 29 | in the benchmark. The data of chart comes from [https://github.com/SlinSo/goTemplateBenchmark](https://github.com/SlinSo/goTemplateBenchmark#full-featured-template-engines-2). 30 | You can find more details and benchmarks from that project. 31 | 32 | 33 | 34 | 35 | ## Install 36 | 37 | ```shell 38 | go get github.com/shiyanhui/hero/hero 39 | 40 | # Hero needs `goimports` to format the generated codes. 41 | go get golang.org/x/tools/cmd/goimports 42 | ``` 43 | 44 | ## Usage 45 | 46 | ```shell 47 | hero [options] 48 | 49 | -source string 50 | the html template file or dir (default "./") 51 | -dest string 52 | generated golang files dir, it will be the same with source if not set 53 | -extensions string 54 | source file extensions, comma splitted if many (default ".html") 55 | -pkgname template 56 | the generated template package name, default is template (default "template") 57 | -watch 58 | whether automatically compile when the source files change 59 | 60 | example: 61 | hero -source="./" 62 | hero -source="$GOPATH/src/app/template" -dest="./" -extensions=".html,.htm" -pkgname="t" -watch 63 | ``` 64 | 65 | ## Quick Start 66 | 67 | Assume that we are going to render a user list `userlist.html`. `index.html` 68 | is the layout, and `user.html` is an item in the list. 69 | 70 | And assumes that they are all under `$GOPATH/src/app/template` 71 | 72 | ### index.html 73 | 74 | ```html 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <%@ body { %> 83 | <% } %> 84 | 85 | 86 | ``` 87 | 88 | ### userlist.html 89 | 90 | ```html 91 | <%: func UserList(userList []string, buffer *bytes.Buffer) %> 92 | 93 | <%~ "index.html" %> 94 | 95 | <%@ body { %> 96 | <% for _, user := range userList { %> 97 | 100 | <% } %> 101 | <% } %> 102 | ``` 103 | 104 | ### user.html 105 | 106 | ```html 107 |
  • 108 | <%= user %> 109 |
  • 110 | ``` 111 | 112 | Then we compile the templates to go code. 113 | 114 | ```shell 115 | hero -source="$GOPATH/src/app/template" 116 | ``` 117 | 118 | We will get three new `.go` files under `$GOPATH/src/app/template`, 119 | i.e. `index.html.go`, `user.html.go` and `userlist.html.go`. 120 | 121 | Then we write a http server in `$GOPATH/src/app/main.go`. 122 | 123 | ### main.go 124 | 125 | ```go 126 | package main 127 | 128 | import ( 129 | "bytes" 130 | "net/http" 131 | 132 | "app/template" 133 | ) 134 | 135 | func main() { 136 | http.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { 137 | var userList = []string { 138 | "Alice", 139 | "Bob", 140 | "Tom", 141 | } 142 | 143 | // Had better use buffer pool. Hero exports `GetBuffer` and `PutBuffer` for this. 144 | // 145 | // For convenience, hero also supports `io.Writer`. For example, you can also define 146 | // the function to `func UserList(userList []string, w io.Writer) (int, error)`, 147 | // and then: 148 | // 149 | // template.UserList(userList, w) 150 | // 151 | buffer := new(bytes.Buffer) 152 | template.UserList(userList, buffer) 153 | w.Write(buffer.Bytes()) 154 | }) 155 | 156 | http.ListenAndServe(":8080", nil) 157 | } 158 | ``` 159 | 160 | At last, start the server and visit `http://localhost:8080/users` in your browser, we will get what we want! 161 | 162 | ## Template syntax 163 | 164 | There are only nine necessary kinds of statements, which are: 165 | 166 | - Function Definition `<%: func define %>` 167 | - Function definition statement defines the function which represents an html file. 168 | - The type of the last parameter in the function defined should be `*bytes.Buffer` for manual buffer management or `io.Writer` for automatic buffer management ( 169 | note: if using `io.Writer` you may optionally specify return values `(int, error)` to handle the result of `io.Writer.Write`). Hero will identify the parameter name 170 | automaticly. 171 | - Example: 172 | - `<%: func UserList(userList []string, buffer *bytes.Buffer) %>` 173 | - `<%: func UserList(userList []string, w io.Writer) %>` 174 | - `<%: func UserList(userList []string, w io.Writer) (int, error) %>` 175 | 176 | - Extend `<%~ "parent template" %>` 177 | - Extend statement states the parent template the current template extends. 178 | - The parent template should be quoted with `""`. 179 | - Example: `<%~ "index.html" >`, which we have mentioned in quick start, too. 180 | 181 | - Include `<%+ "sub template" %>` 182 | - Include statement includes a sub-template to the current template. It works like `#include` in `C++`. 183 | - The sub-template should be quoted with `""`. 184 | - Example: `<%+ "user.html" >`, which we also have mentioned in quick start. 185 | 186 | - Import `<%! go code %>` 187 | - Import statement imports the packages used in the defined function, and it also contains everything that is outside of the defined function. 188 | - Import statement will NOT be inherited by child template. 189 | - Example: 190 | 191 | ```go 192 | <%! 193 | import ( 194 | "fmt" 195 | "strings" 196 | ) 197 | 198 | var a int 199 | 200 | const b = "hello, world" 201 | 202 | func Add(a, b int) int { 203 | return a + b 204 | } 205 | 206 | type S struct { 207 | Name string 208 | } 209 | 210 | func (s S) String() string { 211 | return s.Name 212 | } 213 | %> 214 | ``` 215 | 216 | - Block `<%@ blockName { %> <% } %>` 217 | 218 | - Block statement represents a block. Child template overwrites blocks to extend parent template. 219 | 220 | - Example: 221 | 222 | ```html 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | <%@ body { %> 231 | <% } %> 232 | 233 | 234 | ``` 235 | 236 | - Code `<% go code %>` 237 | 238 | - Code statement states all code inside the defined function. It's just go code. 239 | 240 | - Example: 241 | 242 | ```go 243 | <% for _, user := range userList { %> 244 | <% if user != "Alice" { %> 245 | <%= user %> 246 | <% } %> 247 | <% } %> 248 | 249 | <% 250 | a, b := 1, 2 251 | c := Add(a, b) 252 | %> 253 | ``` 254 | 255 | - Raw Value `<%==[t] variable %>` 256 | 257 | - Raw Value statement will convert the variable to string. 258 | - `t` is the type of variable, hero will find suitable converting method by `t`. Candidates of `t` are: 259 | - `b`: bool 260 | - `i`: int, int8, int16, int32, int64 261 | - `u`: byte, uint, uint8, uint16, uint32, uint64 262 | - `f`: float32, float64 263 | - `s`: string 264 | - `bs`: []byte 265 | - `v`: interface 266 | 267 | Note: 268 | - If `t` is not set, the value of `t` is `s`. 269 | - Had better not use `v`, cause when `t=v`, the converting method is `fmt.Sprintf("%v", variable)` and it is very slow. 270 | - Example: 271 | 272 | ```go 273 | <%== "hello" %> 274 | <%==i 34 %> 275 | <%==u Add(a, b) %> 276 | <%==s user.Name %> 277 | ``` 278 | 279 | - Escaped Value `<%=[t] variable %>` 280 | 281 | - Escaped Value statement is similar with Raw Value statement, but after converting, it will be escaped it with `html.EscapesString`. 282 | - `t` is the same as in `Raw Value Statement`. 283 | - Example: 284 | 285 | ```go 286 | <%= a %> 287 | <%=i a + b %> 288 | <%=u Add(a, b) %> 289 | <%=bs []byte{1, 2} %> 290 | ``` 291 | 292 | - Note `<%# note %>` 293 | 294 | - Note statement add notes to the template. 295 | - It will not be added to the generated go source. 296 | - Example: `<# this is just a note example>`. 297 | 298 | ## License 299 | 300 | Hero is licensed under the Apache License. 301 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Hero 2 | 3 | Hero是一个高性能、强大并且易用的go模板引擎,工作原理是把模板预编译为go代码。Hero目前已经在[bthub.io](http://bthub.io)的线上环境上使用。 4 | 5 | [![GoDoc](https://godoc.org/github.com/shiyanhui/hero?status.svg)](https://godoc.org/github.com/shiyanhui/hero) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/shiyanhui/hero)](https://goreportcard.com/report/github.com/shiyanhui/hero) 7 | 8 | - [Features](#features) 9 | - [Install](#install) 10 | - [Usage](#usage) 11 | - [Quick Start](#quick-start) 12 | - [Template Syntax](#template-syntax) 13 | - [License](#license) 14 | 15 | ## Features 16 | 17 | - 高性能. 18 | - 非常易用. 19 | - 功能强大,支持模板继承和模板include. 20 | - 自动编译. 21 | 22 | ## Performance 23 | 24 | Hero在目前已知的模板引擎中是速度是最快的,并且内存使用是最少的。下面Benchmark图表的数据 25 | 来源于[https://github.com/SlinSo/goTemplateBenchmark](https://github.com/SlinSo/goTemplateBenchmark#full-featured-template-engines-2), 26 | 关于更多的细节和Benchmarks请到上述项目中查看。 27 | 28 | 29 | 30 | 31 | ## Install 32 | 33 | go get github.com/shiyanhui/hero 34 | go get github.com/shiyanhui/hero/hero 35 | 36 | // Hero需要goimports处理生成的go代码,所以需要安装goimports. 37 | go get golang.org/x/tools/cmd/goimports 38 | 39 | ## Usage 40 | 41 | ```shell 42 | hero [options] 43 | 44 | options: 45 | - source: 模板目录,默认为当前目录 46 | - dest: 生成的go代码的目录,如果没有设置的话,和source一样 47 | - pkgname: 生成的go代码包的名称,默认为template 48 | - extensions: source文件的后缀, 如果有多个则用英文逗号隔开, 默认为.html 49 | - watch: 是否监控模板文件改动并自动编译 50 | 51 | example: 52 | hero -source="./" 53 | hero -source="$GOPATH/src/app/template" -watch 54 | ``` 55 | 56 | ## Quick Start 57 | 58 | 假设我们现在要渲染一个用户列表模板`userlist.html`, 它继承自`index.html`, 并且一个用户的模板是`user.html`. 我们还假设所有的模板都在`$GOPATH/src/app/template`目录下。 59 | 60 | ### index.html 61 | 62 | ```html 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | <%@ body { %> 71 | <% } %> 72 | 73 | 74 | ``` 75 | 76 | ### userlist.html 77 | 78 | ```html 79 | <%: func UserList(userList []string, buffer *bytes.Buffer) %> 80 | 81 | <%~ "index.html" %> 82 | 83 | <%@ body { %> 84 | <% for _, user := range userList { %> 85 | 88 | <% } %> 89 | <% } %> 90 | ``` 91 | 92 | ### user.html 93 | 94 | ```html 95 |
  • 96 | <%= user %> 97 |
  • 98 | ``` 99 | 100 | 然后我们编译这些模板: 101 | 102 | ```shell 103 | hero -source="$GOPATH/src/app/template" 104 | ``` 105 | 106 | 编译后,我们将在同一个目录下得到三个go文件,分别是`index.html.go`, `user.html.go` 107 | 和 `userlist.html.go`, 然后我们在http server里边去调用模板: 108 | 109 | ### main.go 110 | 111 | ```go 112 | package main 113 | 114 | import ( 115 | "bytes" 116 | "net/http" 117 | 118 | "github.com/shiyanhui/hero/examples/app/template" 119 | ) 120 | 121 | func main() { 122 | http.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { 123 | var userList = []string{ 124 | "Alice", 125 | "Bob", 126 | "Tom", 127 | } 128 | 129 | buffer := new(bytes.Buffer) 130 | template.UserList(userList, buffer) 131 | 132 | w.Write(buffer.Bytes()) 133 | }) 134 | 135 | http.ListenAndServe(":8080", nil) 136 | } 137 | ``` 138 | 139 | 最后,运行这个http server,访问`http://localhost:8080/users`,我们就能得到我们期待的结果了! 140 | 141 | ## Template syntax 142 | 143 | Hero总共有九种语句,他们分别是: 144 | 145 | - 函数定义语句 `<%: func define %>` 146 | - 该语句定义了该模板所对应的函数,如果一个模板中没有函数定义语句,那么最终结果不会生成对应的函数。 147 | - 该函数最后一个参数必须为`*bytes.Buffer`或者`io.Writer`, hero会自动识别该参数的名字,并把把结果写到该参数里。 148 | - 例: 149 | - `<%: func UserList(userList []string, buffer *bytes.Buffer) %>` 150 | - `<%: func UserList(userList []string, w io.Writer) %>` 151 | - `<%: func UserList(userList []string, w io.Writer) (int, error) %>` 152 | 153 | - 模板继承语句 `<%~ "parent template" %>` 154 | - 该语句声明要继承的模板。 155 | - 例: `<%~ "index.html" >` 156 | 157 | - 模板include语句 `<%+ "sub template" %>` 158 | - 该语句把要include的模板加载进该模板,工作原理和`C++`中的`#include`有点类似。 159 | - 例: `<%+ "user.html" >` 160 | 161 | - 包导入语句 `<%! go code %>` 162 | - 该语句用来声明所有在函数外的代码,包括依赖包导入、全局变量、const等。 163 | 164 | - 该语句不会被子模板所继承 165 | 166 | - 例: 167 | 168 | ```go 169 | <%! 170 | import ( 171 | "fmt" 172 | "strings" 173 | ) 174 | 175 | var a int 176 | 177 | const b = "hello, world" 178 | 179 | func Add(a, b int) int { 180 | return a + b 181 | } 182 | 183 | type S struct { 184 | Name string 185 | } 186 | 187 | func (s S) String() string { 188 | return s.Name 189 | } 190 | %> 191 | ``` 192 | 193 | - 块语句 `<%@ blockName { %> <% } %>` 194 | 195 | - 块语句是用来在子模板中重写父模中的同名块,进而实现模板的继承。 196 | 197 | - 例: 198 | 199 | ```html 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | <%@ body { %> 208 | <% } %> 209 | 210 | 211 | ``` 212 | 213 | - Go代码语句 `<% go code %>` 214 | 215 | - 该语句定义了函数内部的代码部分。 216 | 217 | - 例: 218 | 219 | ```go 220 | <% for _, user := range userList { %> 221 | <% if user != "Alice" { %> 222 | <%= user %> 223 | <% } %> 224 | <% } %> 225 | 226 | <% 227 | a, b := 1, 2 228 | c := Add(a, b) 229 | %> 230 | ``` 231 | 232 | - 原生值语句 `<%==[t] variable %>` 233 | 234 | - 该语句把变量转换为string。 235 | 236 | - `t`是变量的类型,hero会自动根据`t`来选择转换函数。`t`的待选值有: 237 | - `b`: bool 238 | - `i`: int, int8, int16, int32, int64 239 | - `u`: byte, uint, uint8, uint16, uint32, uint64 240 | - `f`: float32, float64 241 | - `s`: string 242 | - `bs`: []byte 243 | - `v`: interface 244 | 245 | 注意: 246 | - 如果`t`没有设置,那么`t`默认为`s`. 247 | - 最好不要使用`v`,因为其对应的转换函数为`fmt.Sprintf("%v", variable)`,该函数很慢。 248 | 249 | - 例: 250 | 251 | ```go 252 | <%== "hello" %> 253 | <%==i 34 %> 254 | <%==u Add(a, b) %> 255 | <%==s user.Name %> 256 | ``` 257 | 258 | - 转义值语句 `<%= statement %>` 259 | 260 | - 该语句把变量转换为string后,又通过`html.EscapesString`记性转义。 261 | - `t`跟上面原生值语句中的`t`一样。 262 | - 例: 263 | 264 | ```go 265 | <%= a %> 266 | <%= a + b %> 267 | <%= Add(a, b) %> 268 | <%= user.Name %> 269 | ``` 270 | 271 | - 注释语句 `<%# note %>` 272 | 273 | - 该语句注释相关模板,注释不会被生成到go代码里边去。 274 | - 例: `<# 这是一个注释 >`. 275 | 276 | ## License 277 | 278 | Hero is licensed under the Apache License. 279 | -------------------------------------------------------------------------------- /bufferpool.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | const buffSize = 10000 9 | 10 | var defaultPool *pool 11 | 12 | func init() { 13 | defaultPool = newPool() 14 | } 15 | 16 | type pool struct { 17 | pool *sync.Pool 18 | ch chan *bytes.Buffer 19 | } 20 | 21 | func newPool() *pool { 22 | p := &pool{ 23 | pool: &sync.Pool{ 24 | New: func() interface{} { 25 | return new(bytes.Buffer) 26 | }, 27 | }, 28 | ch: make(chan *bytes.Buffer, buffSize), 29 | } 30 | 31 | // It's faster with unused channel buffer in go1.7. 32 | // TODO: need removed? 33 | for i := 0; i < buffSize; i++ { 34 | p.ch <- new(bytes.Buffer) 35 | } 36 | 37 | return p 38 | } 39 | 40 | // GetBuffer returns a *bytes.Buffer from sync.Pool. 41 | func GetBuffer() *bytes.Buffer { 42 | return defaultPool.pool.Get().(*bytes.Buffer) 43 | } 44 | 45 | // PutBuffer puts a *bytes.Buffer to the sync.Pool. 46 | func PutBuffer(buffer *bytes.Buffer) { 47 | if buffer == nil { 48 | return 49 | } 50 | 51 | buffer.Reset() 52 | defaultPool.pool.Put(buffer) 53 | } 54 | -------------------------------------------------------------------------------- /bufferpool_test.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var buffer *bytes.Buffer 9 | 10 | func init() { 11 | buffer = new(bytes.Buffer) 12 | } 13 | 14 | func TestNewPool(t *testing.T) { 15 | pool := newPool() 16 | if pool == nil || pool.pool == nil || len(pool.ch) != buffSize { 17 | t.Fail() 18 | } 19 | } 20 | 21 | func TestGetBuffer(t *testing.T) { 22 | if GetBuffer() == nil { 23 | t.Fail() 24 | } 25 | } 26 | 27 | func TestPutBubber(t *testing.T) { 28 | // test for panic 29 | PutBuffer(buffer) 30 | } 31 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | // Hero is a handy, fast and powerful go template engine, which pre-compiles the html templtes to go code. 4 | // It has been used in production environment in [bthub.io](http://bthub.io). 5 | // 6 | // [![GoDoc](https://godoc.org/github.com/shiyanhui/hero?status.svg)](https://godoc.org/github.com/shiyanhui/hero) 7 | // [![Go Report Card](https://goreportcard.com/badge/github.com/shiyanhui/hero)](https://goreportcard.com/report/github.com/shiyanhui/hero) 8 | // 9 | // [中文文档](https://github.com/shiyanhui/hero/blob/master/README_CN.md) 10 | // 11 | // - [Features](#features) 12 | // - [Install](#install) 13 | // - [Usage](#usage) 14 | // - [Quick Start](#quick-start) 15 | // - [Template Syntax](#template-syntax) 16 | // - [License](#license) 17 | // 18 | // ## Features 19 | // 20 | // - High performance. 21 | // - Easy to use. 22 | // - Powerful. template `Extend` and `Include` supported. 23 | // - Auto compiling when files change. 24 | // 25 | // ## Performance 26 | // 27 | // Hero is the fastest and least-memory used among currently known template engines 28 | // in the benchmark. For more details and benchmarks please come to [github.com/SlinSo/goTemplateBenchmark](https://github.com/SlinSo/goTemplateBenchmark). 29 | // 30 | // 31 | // 32 | // 33 | // ## Install 34 | // 35 | // go get github.com/shiyanhui/hero 36 | // go install github.com/shiyanhui/hero/hero 37 | // 38 | // ## Usage 39 | // 40 | // ```shell 41 | // hero [options] 42 | // 43 | // options: 44 | // - source: the html template file or dir. 45 | // - dest: generated golang files dir, it will be the same with source if not set. 46 | // - pkgname: the generated template package name, default is `template`. 47 | // - watch: whether automic compile when the source files change. 48 | // 49 | // example: 50 | // hero -source="./" 51 | // hero -source="$GOPATH/src/app/template" -watch 52 | // ``` 53 | // 54 | // ## Quick Start 55 | // 56 | // Assume that we are going to render a user list `userlist.html`. `index.html` 57 | // is the layout, and `user.html` is an item in the list. 58 | // 59 | // And assumes that they are all under `$GOPATH/src/app/template` 60 | // 61 | // ### index.html 62 | // 63 | // ```html 64 | // 65 | // 66 | // 67 | // 68 | // 69 | // 70 | // 71 | // <%@ body { %> 72 | // <% } %> 73 | // 74 | // 75 | // ``` 76 | // 77 | // ### users.html 78 | // 79 | // ```html 80 | // <%: func UserList(userList []string) *bytes.Buffer %> 81 | // 82 | // <%~ "index.html" %> 83 | // 84 | // <%@ body { %> 85 | // <% for _, user := range userList { %> 86 | // 89 | // <% } %> 90 | // <% } %> 91 | // ``` 92 | // 93 | // ### user.html 94 | // 95 | // ```html 96 | //
  • 97 | // <%= user %> 98 | //
  • 99 | // ``` 100 | // 101 | // Then we compile the templates to go code. 102 | // 103 | // ```shell 104 | // hero -source="$GOPATH/src/app/template" 105 | // ``` 106 | // 107 | // We will get three new `.go` files under `$GOPATH/src/app/template`, 108 | // i.e. `index.html.go`, `user.html.go` and `userlist.html.go`. 109 | // 110 | // Then we write a http server in `$GOPATH/src/app/main.go`. 111 | // 112 | // ### main.go 113 | // 114 | // ```go 115 | // package main 116 | // 117 | // import ( 118 | // "net/http" 119 | // 120 | // "app/template" 121 | // 122 | // "github.com/shiyanhui/hero" 123 | // ) 124 | // 125 | // func main() { 126 | // http.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { 127 | // var userList = []string { 128 | // "Alice", 129 | // "Bob", 130 | // "Tom", 131 | // } 132 | // 133 | // buffer := template.UserList(userList) 134 | // defer hero.PutBuffer(buffer) 135 | // 136 | // w.Write(buffer.Bytes()) 137 | // }) 138 | // 139 | // http.ListenAndServe(":8080", nil) 140 | // } 141 | // ``` 142 | // 143 | // At last, start the server and visit `http://localhost:8080/users` in your browser, we will get what we want! 144 | // 145 | // ## Template syntax 146 | // 147 | // There are only nine necessary kinds of statements, which are: 148 | // 149 | // - Function Definition `<%: func define %>` 150 | // - Function definition statement defines the function which represents a html file. 151 | // - The function defined should return one and only one parameter `*bytes.Buffer`. 152 | // - Example:`<%: func UserList(userList []string) *bytes.Buffer %>`, which we have mentioned in quick start. 153 | // 154 | // - Extend `<%~ "parent template" %>` 155 | // - Extend statement states the parent template the current template extends. 156 | // - The parent template should be quoted with `""`. 157 | // - Example: `<%~ "index.html" >`, which we have mentioned in quick start, too. 158 | // 159 | // - Include `<%+ "sub template" %>` 160 | // - Include statement includes a sub-template to the current template. It works like `#include` in `C++`. 161 | // - The sub-template should be quoted with `""`. 162 | // - Example: `<%+ "user.html" >`, which we also have mentioned in quick start. 163 | // 164 | // - Import `<%! go code %>` 165 | // - Import statement imports the packages used in the defined function, and it also contains everything that is outside of the defined function. 166 | // - Import statement will NOT be inherited by child template. 167 | // - Example: 168 | // 169 | // ```go 170 | // <%! 171 | // import ( 172 | // "fmt" 173 | // "strings" 174 | // ) 175 | // 176 | // var a int 177 | // 178 | // const b = "hello, world" 179 | // 180 | // func Add(a, b int) int { 181 | // return a + b 182 | // } 183 | // 184 | // type S struct { 185 | // Name string 186 | // } 187 | // 188 | // func (s S) String() string { 189 | // return s.Name 190 | // } 191 | // %> 192 | // ``` 193 | // 194 | // - Block `<%@ blockName { %> <% } %>` 195 | // 196 | // - Block statement represents a block. Child template overwrites blocks to extend parent template. 197 | // 198 | // - Example: 199 | // 200 | // ```html 201 | // 202 | // 203 | // 204 | // 205 | // 206 | // 207 | // 208 | // <%@ body { %> 209 | // <% } %> 210 | // 211 | // 212 | // ``` 213 | // 214 | // - Code `<% go code %>` 215 | // 216 | // - Code statement states all code inside the defined function. It's just go code. 217 | // 218 | // - Example: 219 | // 220 | // ```go 221 | // <% for _, user := userList { %> 222 | // <% if user != "Alice" { %> 223 | // <%= user %> 224 | // <% } %> 225 | // <% } %> 226 | // 227 | // <% 228 | // a, b := 1, 2 229 | // c := Add(a, b) 230 | // %> 231 | // ``` 232 | // 233 | // - Raw Value `<%==[t] variable %>` 234 | // 235 | // - Raw Value statement will convert the variable to string. 236 | // - `t` is the type of varible, hero will find suitable converting method by `t`. Condidates of `t` are: 237 | // - `b`: bool 238 | // - `i`: int, int8, int16, int32, int64 239 | // - `u`: byte, uint, uint8, uint16, uint32, uint64 240 | // - `f`: float32, float64 241 | // - `s`: string 242 | // - `bs`: []byte 243 | // - `v`: interface 244 | // 245 | // Note: 246 | // - If `t` is not set, the value of `t` is `s`. 247 | // - Had better not use `v`, cause when `t=v`, the converting method is `fmt.Sprintf("%v", variable)` and it is very slow. 248 | // - Example: 249 | // 250 | // ```go 251 | // <%== "hello" %> 252 | // <%==i 34 %> 253 | // <%==u Add(a, b) %> 254 | // <%==s user.Name %> 255 | // ``` 256 | // 257 | // - Escaped Value `<%=[t] variable %>` 258 | // 259 | // - Escaped Value statement is similar with Raw Value statement, but after converting, it will escaped it with `html.EscapesString`. 260 | // - `t` is the same with that of `Raw Value Statement`. 261 | // - Example: 262 | // 263 | // ```go 264 | // <%= a %> 265 | // <%=i a + b %> 266 | // <%=u Add(a, b) %> 267 | // <%=bs []byte{1, 2} %> 268 | // ``` 269 | // 270 | // - Note `<%# note %>` 271 | // 272 | // - Note statement add notes to the template. 273 | // - It will not be added to the generated go source. 274 | // - Example: `<# this is just a note example>`. 275 | // 276 | // ## License 277 | // 278 | // Hero is licensed under the Apache License. 279 | -------------------------------------------------------------------------------- /examples/app/app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/shiyanhui/hero/examples/app/template" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { 13 | var userList = []string{ 14 | "Alice", 15 | "Bob", 16 | "Tom", 17 | } 18 | 19 | // Had better use buffer sync.Pool. 20 | // Hero exports GetBuffer and PutBuffer for this. 21 | // 22 | // buffer := hero.GetBuffer() 23 | // defer hero.PutBuffer(buffer) 24 | buffer := new(bytes.Buffer) 25 | template.UserList(userList, buffer) 26 | 27 | if _, err := w.Write(buffer.Bytes()); err != nil { 28 | log.Printf("ERR: %s\n", err) 29 | } 30 | }) 31 | 32 | http.HandleFunc("/users2", func(w http.ResponseWriter, req *http.Request) { 33 | var userList = []string{ 34 | "Alice", 35 | "Bob", 36 | "Tom", 37 | } 38 | 39 | // using an io.Writer for automatic buffer management (i.e. hero built-in buffer pool) 40 | template.UserListToWriter(userList, w) 41 | }) 42 | 43 | log.Fatal(http.ListenAndServe(":8080", nil)) 44 | } 45 | -------------------------------------------------------------------------------- /examples/app/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%@ body { %> 9 | <% } %> 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/app/template/index.html.go: -------------------------------------------------------------------------------- 1 | // Code generated by hero. 2 | // source: /Users/Lime/Documents/workspace/GoProject/src/github.com/shiyanhui/hero/examples/app/template/index.html 3 | // DO NOT EDIT! 4 | package template 5 | -------------------------------------------------------------------------------- /examples/app/template/user.html: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= user %> 3 |
  • 4 | -------------------------------------------------------------------------------- /examples/app/template/user.html.go: -------------------------------------------------------------------------------- 1 | // Code generated by hero. 2 | // source: /Users/Lime/Documents/workspace/GoProject/src/github.com/shiyanhui/hero/examples/app/template/user.html 3 | // DO NOT EDIT! 4 | package template 5 | -------------------------------------------------------------------------------- /examples/app/template/userlist.html: -------------------------------------------------------------------------------- 1 | <%: func UserList(userList []string, buffer *bytes.Buffer) %> 2 | 3 | <%~ "index.html" %> 4 | 5 | <%@ body { %> 6 | <% for _, user := range userList { %> 7 | 10 | <% } %> 11 | <% } %> 12 | -------------------------------------------------------------------------------- /examples/app/template/userlist.html.go: -------------------------------------------------------------------------------- 1 | // Code generated by hero. 2 | // source: /Users/Lime/Documents/workspace/GoProject/src/github.com/shiyanhui/hero/examples/app/template/userlist.html 3 | // DO NOT EDIT! 4 | package template 5 | 6 | import ( 7 | "bytes" 8 | 9 | "github.com/shiyanhui/hero" 10 | ) 11 | 12 | func UserList(userList []string, buffer *bytes.Buffer) { 13 | buffer.WriteString(` 14 | 15 | 16 | 17 | 18 | 19 | 20 | `) 21 | for _, user := range userList { 22 | buffer.WriteString(` 23 | 34 | `) 35 | } 36 | 37 | buffer.WriteString(` 38 | 39 | 40 | `) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /examples/app/template/userlistwriter.html: -------------------------------------------------------------------------------- 1 | <%: func UserListToWriter(userList []string, w io.Writer) (int, error)%> 2 | 3 | <%~ "index.html" %> 4 | 5 | <%@ body { %> 6 | <% for _, user := range userList { %> 7 | 10 | <% } %> 11 | <% } %> 12 | -------------------------------------------------------------------------------- /examples/app/template/userlistwriter.html.go: -------------------------------------------------------------------------------- 1 | // Code generated by hero. 2 | // source: /Users/Lime/Documents/workspace/GoProject/src/github.com/shiyanhui/hero/examples/app/template/userlistwriter.html 3 | // DO NOT EDIT! 4 | package template 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/shiyanhui/hero" 10 | ) 11 | 12 | func UserListToWriter(userList []string, w io.Writer) (int, error) { 13 | _buffer := hero.GetBuffer() 14 | defer hero.PutBuffer(_buffer) 15 | _buffer.WriteString(` 16 | 17 | 18 | 19 | 20 | 21 | 22 | `) 23 | for _, user := range userList { 24 | _buffer.WriteString(` 25 | 36 | `) 37 | } 38 | 39 | _buffer.WriteString(` 40 | 41 | 42 | `) 43 | return w.Write(_buffer.Bytes()) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "go/ast" 8 | "go/build" 9 | "go/parser" 10 | "go/token" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "path/filepath" 15 | "reflect" 16 | "strings" 17 | "sync" 18 | ) 19 | 20 | const ( 21 | TypeBytesBuffer = "bytes.Buffer" 22 | TypeIOWriter = "io.Writer" 23 | ) 24 | 25 | var errExpectParam = errors.New( 26 | "The last parameter should be *bytes.Buffer or io.Writer type", 27 | ) 28 | 29 | func formatMap(t, bufName string) string { 30 | switch t { 31 | case String: 32 | return "%s" 33 | case Interface: 34 | return "fmt.Sprintf(\"%%v\", %s)" 35 | } 36 | 37 | m := map[string]string{ 38 | Int: "hero.FormatInt(int64(%%s), %s)", 39 | Uint: "hero.FormatUint(uint64(%%s), %s)", 40 | Float: "hero.FormatFloat(float64(%%s), %s)", 41 | Bool: "hero.FormatBool(%%s, %s)", 42 | } 43 | 44 | format, ok := m[t] 45 | if !ok { 46 | log.Fatal("Unknown type ", t) 47 | } 48 | return fmt.Sprintf(format, bufName) 49 | } 50 | 51 | func writeToFile(path string, buffer *bytes.Buffer) { 52 | err := ioutil.WriteFile(path, buffer.Bytes(), os.ModePerm) 53 | if err != nil { 54 | panic(err) 55 | } 56 | } 57 | 58 | func genAbsPath(path string) string { 59 | if !filepath.IsAbs(path) { 60 | var err error 61 | if path, err = filepath.Abs(path); err != nil { 62 | log.Fatal(err) 63 | } 64 | } 65 | return path 66 | } 67 | 68 | func checkFileError(trimPath,file string, err error) { 69 | if err != nil { 70 | log.Fatal(strings.TrimPrefix(file,trimPath + "/") + ": " + err.Error()) 71 | } 72 | } 73 | 74 | // parseDefinition parses the function definition. 75 | func parseDefinition(definition string) (*ast.FuncDecl, error) { 76 | src := fmt.Sprintf("package hero\n%s", definition) 77 | 78 | fset := token.NewFileSet() 79 | file, err := parser.ParseFile(fset, "", src, parser.AllErrors) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | funcDecl, ok := file.Decls[0].(*ast.FuncDecl) 85 | if !ok { 86 | return nil, errors.New("Definition is not function type") 87 | } 88 | return funcDecl, nil 89 | } 90 | 91 | // parseParams parses parameters in the function definition. 92 | func parseParams(funcDecl *ast.FuncDecl) (name, t string, err error) { 93 | params := funcDecl.Type.Params.List 94 | if len(params) == 0 { 95 | err = errors.New( 96 | "Definition parameters should not be empty", 97 | ) 98 | return 99 | } 100 | 101 | lastParam := params[len(params)-1] 102 | 103 | expr := lastParam.Type 104 | if starExpr, ok := expr.(*ast.StarExpr); ok { 105 | expr = starExpr.X 106 | } 107 | 108 | selectorExpr, ok := expr.(*ast.SelectorExpr) 109 | if !ok { 110 | err = errExpectParam 111 | return 112 | } 113 | 114 | t = fmt.Sprintf("%s.%s", selectorExpr.X, selectorExpr.Sel) 115 | if t != TypeBytesBuffer && t != TypeIOWriter { 116 | err = fmt.Errorf( 117 | "'%s' expected to be '%s' or '%s'", 118 | t, TypeBytesBuffer, TypeIOWriter, 119 | ) 120 | return 121 | } 122 | 123 | if n := len(lastParam.Names); n > 0 { 124 | name = lastParam.Names[n-1].Name 125 | } 126 | 127 | return 128 | } 129 | 130 | // parseResults parses the returned results in the function definition. 131 | func parseResults(funcDecl *ast.FuncDecl) (types []string) { 132 | if results := funcDecl.Type.Results; results != nil { 133 | for _, field := range results.List { 134 | types = append(types, fmt.Sprintf("%s", field.Type)) 135 | } 136 | } 137 | return 138 | } 139 | 140 | func escapeBackQuote(str string) string { 141 | return strings.Replace(str, "`", "`+\"`\"+`", -1) 142 | } 143 | 144 | // gen generates code to buffer. 145 | func gen(n *node, buffer *bytes.Buffer, bufName string) { 146 | for _, child := range n.children { 147 | switch child.t { 148 | case TypeCode: 149 | buffer.Write(child.chunk.Bytes()) 150 | case TypeHTML: 151 | buffer.WriteString(fmt.Sprintf( 152 | "%s.WriteString(`%s`)", 153 | bufName, 154 | escapeBackQuote(child.chunk.String()), 155 | )) 156 | case TypeRawValue, TypeEscapedValue: 157 | var format string 158 | 159 | switch child.subtype { 160 | case Int, Uint, Float, Bool, String, Interface: 161 | format = formatMap(child.subtype, bufName) 162 | if child.subtype != String && 163 | child.subtype != Interface { 164 | goto WriteFormat 165 | } 166 | case Bytes: 167 | if child.t == TypeRawValue { 168 | format = fmt.Sprintf("%s.Write(%%s)", bufName) 169 | goto WriteFormat 170 | } 171 | format = "*(*string)(unsafe.Pointer(&(%s)))" 172 | default: 173 | log.Fatal("unknown value type: " + child.subtype) 174 | } 175 | 176 | if child.t == TypeEscapedValue { 177 | format = fmt.Sprintf( 178 | "hero.EscapeHTML(%s, %s)", format, bufName, 179 | ) 180 | } else { 181 | format = fmt.Sprintf( 182 | "%s.WriteString(%s)", bufName, format, 183 | ) 184 | } 185 | 186 | WriteFormat: 187 | buffer.WriteString(fmt.Sprintf(format, child.chunk.String())) 188 | case TypeBlock, TypeInclude: 189 | gen(child, buffer, bufName) 190 | default: 191 | continue 192 | } 193 | 194 | buffer.WriteByte(BreakLine) 195 | } 196 | } 197 | 198 | // Generate generates Go code from source to test. pkgName represents the 199 | // package name of the generated code. 200 | func Generate(source, dest, pkgName string, extensions []string) { 201 | defer cleanGlobal() 202 | 203 | source, dest = genAbsPath(source), genAbsPath(dest) 204 | sourceDir := source 205 | 206 | srcStat, err := os.Stat(source) 207 | checkFileError("",source,err) 208 | 209 | fmt.Println("Parsing...") 210 | if srcStat.IsDir() { 211 | parseDir(source, extensions) 212 | } else { 213 | sourceDir = filepath.Dir(source) 214 | source, file := filepath.Split(source) 215 | parseFile(source, file) 216 | } 217 | rebuild() 218 | 219 | destStat, err := os.Stat(dest) 220 | if os.IsNotExist(err) { 221 | if err = os.MkdirAll(dest, os.ModePerm); err != nil { 222 | log.Fatal(err) 223 | } 224 | } else if !destStat.IsDir() { 225 | if srcStat.IsDir() { 226 | log.Fatal(dest + " is not a directory") 227 | } 228 | dest = filepath.Dir(dest) 229 | } else if err != nil { 230 | log.Fatal(err) 231 | } 232 | 233 | fmt.Println("Generating...") 234 | 235 | var wg sync.WaitGroup 236 | for path, n := range parsedNodes { 237 | wg.Add(1) 238 | 239 | fileName := filepath.Join(dest, fmt.Sprintf( 240 | "%s.go", 241 | strings.Join(strings.Split( 242 | path[len(sourceDir)+1:], 243 | string(filepath.Separator), 244 | ), "_"), 245 | )) 246 | 247 | go func(n *node, source, fileName string) { 248 | defer wg.Done() 249 | 250 | buffer := bytes.NewBufferString(` 251 | // Code generated by hero. 252 | `) 253 | buffer.WriteString(fmt.Sprintf("// source: %s", strings.TrimPrefix(source, build.Default.GOPATH+"/src/"))) 254 | buffer.WriteString(` 255 | // DO NOT EDIT! 256 | `) 257 | buffer.WriteString(fmt.Sprintf("package %s\n", pkgName)) 258 | buffer.WriteString(` 259 | import "html" 260 | import "unsafe" 261 | 262 | import "github.com/shiyanhui/hero" 263 | `) 264 | 265 | imports := n.childrenByType(TypeImport) 266 | for _, item := range imports { 267 | buffer.Write(item.chunk.Bytes()) 268 | } 269 | 270 | definitions := n.childrenByType(TypeDefinition) 271 | if len(definitions) == 0 { 272 | writeToFile(fileName, buffer) 273 | return 274 | } 275 | 276 | definition := definitions[0].chunk.String() 277 | 278 | funcDecl, err := parseDefinition(definition) 279 | checkFileError(sourceDir,source,err) 280 | 281 | buffer.WriteString(definition) 282 | buffer.WriteString(`{ 283 | `) 284 | 285 | paramName, paramType, err := parseParams(funcDecl) 286 | checkFileError(sourceDir,source,err) 287 | 288 | if paramType == TypeIOWriter { 289 | bufName := "_buffer" 290 | 291 | buffer.WriteString( 292 | fmt.Sprintf( 293 | "%s := hero.GetBuffer()\ndefer hero.PutBuffer(%s)\n", 294 | bufName, bufName, 295 | ), 296 | ) 297 | gen(n, buffer, bufName) 298 | 299 | results, ret := parseResults(funcDecl), "" 300 | if reflect.DeepEqual(results, []string{"int", "error"}) { 301 | ret = "return" 302 | } 303 | 304 | buffer.WriteString( 305 | fmt.Sprintf( 306 | "%s %s.Write(%s.Bytes())\n", 307 | ret, paramName, bufName, 308 | ), 309 | ) 310 | } else { 311 | gen(n, buffer, paramName) 312 | } 313 | 314 | buffer.WriteString(` 315 | }`) 316 | 317 | writeToFile(fileName, buffer) 318 | }(n, path, fileName) 319 | } 320 | wg.Wait() 321 | 322 | fmt.Println("Executing goimports...") 323 | execCommand("goimports -w " + dest) 324 | 325 | fmt.Println("Executing go vet...") 326 | execCommand("go vet " + dest) 327 | } 328 | -------------------------------------------------------------------------------- /generator_test.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | var replacer = regexp.MustCompile(`\s`) 15 | 16 | func TestWriteToFile(t *testing.T) { 17 | path := "/tmp/hero.test" 18 | content := "hello, hero" 19 | 20 | buffer := bytes.NewBufferString(content) 21 | writeToFile(path, buffer) 22 | 23 | defer os.Remove(path) 24 | 25 | if _, err := os.Stat(path); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if c, err := ioutil.ReadFile(path); err != nil { 30 | t.Fatal(err) 31 | } else if string(c) != content { 32 | t.Fatalf("want: %s got: %s", content, c) 33 | } 34 | } 35 | 36 | func TestGenAbsPath(t *testing.T) { 37 | dir, err := filepath.Abs("./") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | parts := strings.Split(dir, string(filepath.Separator)) 43 | parent := strings.Join(parts[:len(parts)-1], string(filepath.Separator)) 44 | 45 | root := os.Args[0] 46 | for root != filepath.Dir(root) { 47 | root = filepath.Dir(root) 48 | } 49 | 50 | cases := []struct { 51 | in string 52 | out string 53 | }{ 54 | {in: "/", out: root}, 55 | {in: ".", out: dir}, 56 | {in: "../", out: parent}, 57 | } 58 | 59 | for _, c := range cases { 60 | got := genAbsPath(c.in) 61 | if got != c.out { 62 | t.Errorf("want: %s got: %s", c.out, got) 63 | } 64 | } 65 | } 66 | 67 | func TestGenerate(t *testing.T) { 68 | Generate(rootDir, rootDir, "template") 69 | 70 | cases := []struct { 71 | file string 72 | code string 73 | }{ 74 | {file: "index.html.go", code: ` 75 | // Code generated by hero. 76 | // source: ` + filepath.Join(rootDir, "index.html") + ` 77 | // DO NOT EDIT! 78 | package template 79 | `}, 80 | {file: "item.html.go", code: ` 81 | // Code generated by hero. 82 | // source: ` + filepath.Join(rootDir, "item.html") + ` 83 | // DO NOT EDIT! 84 | package template 85 | `}, 86 | {file: "list.html.go", code: ` 87 | // Code generated by hero. 88 | // source: ` + filepath.Join(rootDir, "list.html") + ` 89 | // DO NOT EDIT! 90 | package template 91 | 92 | import ( 93 | "bytes" 94 | 95 | "github.com/shiyanhui/hero" 96 | ) 97 | 98 | func Add(a, b int) int { 99 | return a + b 100 | } 101 | func UserList(userList []string, buffer *bytes.Buffer) { 102 | buffer.WriteString(` + "`" + ` 103 | 104 | 105 | 106 | 107 | 108 | ` + "`" + `) 109 | for _, user := range userList { 110 | buffer.WriteString(` + "`" + ` 111 |
    112 | 115 | ` + "`" + `) 116 | buffer.WriteString(user) 117 | buffer.WriteString(` + "`" + ` 118 | 119 |
    120 | ` + "`" + `) 121 | 122 | } 123 | 124 | buffer.WriteString(` + "`" + ` 125 | 126 | 127 | ` + "`" + `) 128 | 129 | } 130 | `}, 131 | {file: "listwriter.html.go", code: ` 132 | // Code generated by hero. 133 | // source: ` + filepath.Join(rootDir, "listwriter.html") + ` 134 | // DO NOT EDIT! 135 | package template 136 | 137 | import ( 138 | "io" 139 | 140 | "github.com/shiyanhui/hero" 141 | ) 142 | 143 | func UserListToWriter(userList []string, w io.Writer) { 144 | _buffer := hero.GetBuffer() 145 | defer hero.PutBuffer(_buffer) 146 | _buffer.WriteString(` + "`" + ` 147 | 148 | 149 | 150 | 151 | 152 | ` + "`" + `) 153 | for _, user := range userList { 154 | _buffer.WriteString(` + "`" + ` 155 |
    156 | 159 | ` + "`" + `) 160 | _buffer.WriteString(user) 161 | _buffer.WriteString(` + "`" + ` 162 | 163 |
    164 | ` + "`" + `) 165 | 166 | } 167 | 168 | _buffer.WriteString(` + "`" + ` 169 | 170 | 171 | ` + "`" + `) 172 | w.Write(_buffer.Bytes()) 173 | } 174 | `}, 175 | {file: "listwriterresult.html.go", code: ` 176 | // Code generated by hero. 177 | // source: ` + filepath.Join(rootDir, "listwriterresult.html") + ` 178 | // DO NOT EDIT! 179 | package template 180 | 181 | import ( 182 | "io" 183 | 184 | "github.com/shiyanhui/hero" 185 | ) 186 | 187 | func UserListToWriterWithResult(userList []string, w io.Writer) (n int, err error) { 188 | _buffer := hero.GetBuffer() 189 | defer hero.PutBuffer(_buffer) 190 | _buffer.WriteString(` + "`" + ` 191 | 192 | 193 | 194 | 195 | 196 | ` + "`" + `) 197 | for _, user := range userList { 198 | _buffer.WriteString(` + "`" + ` 199 |
    200 | 203 | ` + "`" + `) 204 | _buffer.WriteString(user) 205 | _buffer.WriteString(` + "`" + ` 206 | 207 |
    208 | ` + "`" + `) 209 | 210 | } 211 | 212 | _buffer.WriteString(` + "`" + ` 213 | 214 | 215 | ` + "`" + `) 216 | return w.Write(_buffer.Bytes()) 217 | } 218 | `}, 219 | } 220 | 221 | for _, c := range cases { 222 | content, err := ioutil.ReadFile(filepath.Join(rootDir, c.file)) 223 | if err != nil { 224 | t.Error(err) 225 | continue 226 | } 227 | got := replacer.ReplaceAll(content, nil) 228 | want := []byte(replacer.ReplaceAllString(c.code, "")) 229 | if !reflect.DeepEqual(got, want) { 230 | t.Errorf("\nfile: %s\n\nwant:\n%s\n\ngot:\n%s\n", c.file, want, got) 231 | } 232 | } 233 | } 234 | 235 | func TestGen(t *testing.T) { 236 | root := parseFile(rootDir, "list.html") 237 | buffer := new(bytes.Buffer) 238 | bufName := "_buffer" 239 | 240 | gen(root, buffer, bufName) 241 | 242 | if buffer.String() == replacer.ReplaceAllString( 243 | `for _, user := range userList {}`, "") { 244 | t.Fail() 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /hero/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/howeyc/fsnotify" 13 | "github.com/shiyanhui/hero" 14 | ) 15 | 16 | var ( 17 | watch bool 18 | source string 19 | dest string 20 | pkgName string 21 | extension string 22 | ) 23 | 24 | func init() { 25 | flag.StringVar( 26 | &source, 27 | "source", 28 | "./", 29 | "the html template file or dir", 30 | ) 31 | flag.StringVar( 32 | &dest, 33 | "dest", 34 | "", 35 | "generated golang files dir, it will be the same with source if not set", 36 | ) 37 | flag.StringVar( 38 | &extension, 39 | "extensions", 40 | ".html", 41 | "source file extensions, comma splitted if many", 42 | ) 43 | flag.StringVar( 44 | &pkgName, 45 | "pkgname", 46 | "template", 47 | "the generated template package name, default is `template`", 48 | ) 49 | flag.BoolVar( 50 | &watch, 51 | "watch", 52 | false, 53 | "whether automatically compile when the source files change", 54 | ) 55 | } 56 | 57 | func watchFile(watcher *fsnotify.Watcher, path string) { 58 | if err := watcher.Watch(path); err != nil { 59 | log.Fatal(err) 60 | } 61 | } 62 | 63 | func main() { 64 | flag.Parse() 65 | 66 | if dest == "" { 67 | dest = source 68 | } 69 | 70 | extensions := strings.Split(extension, ",") 71 | for i, item := range extensions { 72 | extensions[i] = strings.TrimSpace(item) 73 | } 74 | 75 | hero.Generate(source, dest, pkgName, extensions) 76 | 77 | if !watch { 78 | return 79 | } 80 | 81 | watcher, err := fsnotify.NewWatcher() 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | go func() { 87 | for ev := range watcher.Event { 88 | if hero.CheckExtension(ev.Name, extensions) && 89 | (ev.IsDelete() || ev.IsModify() || ev.IsRename()) { 90 | hero.Generate(source, dest, pkgName, extensions) 91 | } 92 | } 93 | }() 94 | 95 | watchFile(watcher, source) 96 | 97 | stat, _ := os.Stat(source) 98 | if stat.IsDir() { 99 | filepath.Walk(source, func( 100 | path string, _ os.FileInfo, err error) error { 101 | 102 | stat, _ := os.Stat(path) 103 | if stat.IsDir() { 104 | watchFile(watcher, path) 105 | } 106 | return nil 107 | }) 108 | } 109 | 110 | done := make(chan os.Signal, 1) 111 | signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) 112 | 113 | <-done 114 | 115 | watcher.Close() 116 | } 117 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | const ( 12 | TypeImport = iota 13 | TypeDefinition 14 | TypeExtend 15 | TypeInclude 16 | TypeBlock 17 | TypeCode 18 | TypeEscapedValue 19 | TypeRawValue 20 | TypeNote 21 | TypeHTML 22 | TypeRoot 23 | ) 24 | 25 | const ( 26 | Bool = "b" 27 | Int = "i" 28 | Uint = "u" 29 | Float = "f" 30 | String = "s" 31 | Bytes = "bs" 32 | Interface = "v" 33 | ) 34 | 35 | const ( 36 | OpenBrace = '{' 37 | CloseBrace = '}' 38 | LT = '<' 39 | GT = '>' 40 | Percent = '%' 41 | Exclamation = '!' 42 | Colon = ':' 43 | Tilde = '~' 44 | Plus = '+' 45 | Equal = '=' 46 | At = '@' 47 | Pound = '#' 48 | Space = ' ' 49 | BreakLine = '\n' 50 | ) 51 | 52 | var prefixTypeMap = map[byte]uint8{ 53 | Exclamation: TypeImport, 54 | Colon: TypeDefinition, 55 | Tilde: TypeExtend, 56 | Plus: TypeInclude, 57 | Equal: TypeEscapedValue, 58 | At: TypeBlock, 59 | Pound: TypeNote, 60 | } 61 | 62 | var ( 63 | openTag = []byte{LT, Percent} // <% 64 | closeTag = []byte{Percent, GT} // %> 65 | openBraceTag = []byte{OpenBrace} // { 66 | ) 67 | 68 | var parsedNodes map[string]*node 69 | var dependencies *sort 70 | 71 | func init() { 72 | cleanGlobal() 73 | } 74 | 75 | func cleanGlobal() { 76 | parsedNodes = make(map[string]*node) 77 | dependencies = newSort() 78 | } 79 | 80 | type node struct { 81 | t uint8 82 | subtype string 83 | children []*node 84 | chunk *bytes.Buffer 85 | } 86 | 87 | func newNode(t uint8, chunk []byte) *node { 88 | n := &node{ 89 | t: t, 90 | children: make([]*node, 0), 91 | chunk: new(bytes.Buffer), 92 | } 93 | 94 | if chunk != nil { 95 | n.chunk.Write(chunk) 96 | } 97 | 98 | return n 99 | } 100 | 101 | func splitByEndBlock(content []byte) ([]byte, []byte) { 102 | for i, open := 0, 0; i < len(content); i++ { 103 | switch content[i] { 104 | case OpenBrace: 105 | open++ 106 | case CloseBrace: 107 | open-- 108 | } 109 | 110 | if open == -1 { 111 | j := bytes.LastIndex(content[:i], openTag) 112 | k := bytes.Index(content[i+1:], closeTag) 113 | 114 | if j == -1 || k == -1 || 115 | len(bytes.TrimSpace(content[j+2:i+1+k])) != 1 { 116 | goto Panic 117 | } 118 | return content[:j], content[i+k+3:] 119 | } 120 | } 121 | 122 | Panic: 123 | panic("invalid endblock") 124 | } 125 | 126 | func (n *node) insert(dir, subpath string, content []byte) { 127 | path, err := filepath.Abs(filepath.Join(dir, subpath)) 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | for len(content) > 0 { 133 | i := bytes.Index(content, openTag) 134 | if i == -1 { 135 | i = len(content) 136 | } 137 | 138 | if i != 0 { 139 | c := bytes.TrimSpace(content[:i]) 140 | if len(c) > 0 { 141 | n.children = append( 142 | n.children, 143 | newNode(TypeHTML, content[:i]), 144 | ) 145 | } 146 | content = content[i:] 147 | continue 148 | } 149 | 150 | // starts with "<%" 151 | content = content[2:] 152 | 153 | i = bytes.Index(content, closeTag) 154 | if i == -1 { 155 | log.Fatalf("'<%%' not closed in file `%s`", path) 156 | } 157 | 158 | switch content[0] { 159 | case Exclamation, Colon, Pound: 160 | t, c := prefixTypeMap[content[0]], content[1:i] 161 | if len(bytes.TrimSpace(c)) > 0 { 162 | n.children = append(n.children, newNode(t, c)) 163 | } 164 | case Equal: 165 | var ( 166 | t uint8 167 | subtype string 168 | c []byte 169 | ) 170 | 171 | if content[0] == Equal && content[1] == Equal { 172 | t, c = TypeRawValue, content[2:i] 173 | } else { 174 | t, c = TypeEscapedValue, content[1:i] 175 | } 176 | 177 | parts := bytes.Split(c, []byte{Space}) 178 | if len(parts) > 0 { 179 | subtype = string(parts[0]) 180 | if subtype == "" || subtype == String { 181 | subtype = String 182 | } else if subtype != Int && subtype != Uint && 183 | subtype != Float && subtype != Bool && 184 | subtype != Bytes && subtype != Interface { 185 | log.Fatalf("unknown value type %s", subtype) 186 | } 187 | 188 | c = bytes.TrimSpace(bytes.Join(parts[1:], []byte{Space})) 189 | if len(c) > 0 { 190 | child := newNode(t, bytes.TrimSpace(c)) 191 | child.subtype = subtype 192 | 193 | n.children = append(n.children, child) 194 | goto ResetContent 195 | } 196 | } 197 | 198 | log.Fatal("lack of variable name") 199 | case Tilde, Plus: 200 | c := bytes.TrimSpace(content[1:i]) 201 | 202 | parent := string(c[1 : len(c)-1]) 203 | if !filepath.IsAbs(parent) { 204 | if parent, err = filepath.Abs( 205 | filepath.Join(dir, parent)); err != nil { 206 | log.Fatal(err) 207 | } 208 | } 209 | 210 | n.children = append( 211 | n.children, 212 | newNode(prefixTypeMap[content[0]], []byte(parent)), 213 | ) 214 | 215 | dependencies.addVertices(parent, path) 216 | dependencies.addEdge(parent, path) 217 | case At: 218 | chunk := bytes.TrimSpace(content[1:i]) 219 | if !bytes.HasSuffix(chunk, openBraceTag) { 220 | log.Fatalf("block not ended with `{` in file `%s`", path) 221 | } 222 | 223 | child := newNode( 224 | TypeBlock, 225 | bytes.TrimSpace(chunk[:len(chunk)-1]), 226 | ) 227 | 228 | blockName := child.chunk.String() 229 | if b := n.findBlockByName(blockName); b != nil { 230 | log.Fatalf("duplicate block %s in file `%s`", blockName, path) 231 | } 232 | 233 | var childContent []byte 234 | 235 | childContent, content = splitByEndBlock(content[i+2:]) 236 | child.insert(dir, subpath, childContent) 237 | n.children = append(n.children, child) 238 | continue 239 | default: 240 | n.children = append( 241 | n.children, newNode(TypeCode, content[:i]), 242 | ) 243 | } 244 | 245 | ResetContent: 246 | content = content[i+2:] 247 | } 248 | } 249 | 250 | func (n *node) childrenByType(t uint8) []*node { 251 | var children []*node 252 | 253 | for _, child := range n.children { 254 | if child.t == t { 255 | children = append(children, child) 256 | } 257 | } 258 | 259 | return children 260 | } 261 | 262 | func (n *node) findBlockByName(name string) *node { 263 | for _, child := range n.children { 264 | if child.t == TypeBlock && child.chunk.String() == name { 265 | return child 266 | } 267 | } 268 | return nil 269 | } 270 | 271 | func (n *node) rebuild() { 272 | var pNode *node 273 | 274 | nodes := n.childrenByType(TypeExtend) 275 | if len(nodes) > 0 { 276 | pNode = parsedNodes[nodes[0].chunk.String()] 277 | } 278 | 279 | if pNode != nil { 280 | var children []*node 281 | 282 | for _, t := range []uint8{TypeImport, TypeDefinition} { 283 | children = append(children, n.childrenByType(t)...) 284 | } 285 | 286 | for _, child := range pNode.children { 287 | switch child.t { 288 | case TypeHTML, TypeCode, TypeEscapedValue, 289 | TypeRawValue, TypeInclude: 290 | children = append(children, child) 291 | case TypeBlock: 292 | block := n.findBlockByName(child.chunk.String()) 293 | if block != nil { 294 | block.rebuild() 295 | children = append(children, block) 296 | } 297 | } 298 | } 299 | 300 | n.children = children 301 | return 302 | } 303 | 304 | newChildren := make([]*node, 0) 305 | for _, child := range n.children { 306 | switch child.t { 307 | case TypeInclude: 308 | key := child.chunk.String() 309 | includeNode, ok := parsedNodes[key] 310 | if !ok { 311 | var keys []string 312 | for k := range parsedNodes { 313 | keys = append(keys, k) 314 | } 315 | log.Fatalf("node \"%s\" not found. have: %v", key, keys) 316 | } 317 | 318 | for _, t := range []uint8{ 319 | TypeExtend, 320 | TypeInclude, 321 | TypeDefinition} { 322 | if len(includeNode.childrenByType(t)) != 0 { 323 | log.Fatalf( 324 | "there shouldn't be any of statement of Extend, "+ 325 | "Include or Definition in sub-template %s", 326 | key, 327 | ) 328 | } 329 | } 330 | 331 | for _, childNode := range includeNode.children { 332 | if childNode.t == TypeImport { 333 | newChildren = append([]*node{childNode}, newChildren...) 334 | } else { 335 | newChildren = append(newChildren, childNode) 336 | } 337 | } 338 | case TypeBlock: 339 | child.rebuild() 340 | fallthrough 341 | default: 342 | newChildren = append(newChildren, child) 343 | } 344 | } 345 | 346 | n.children = newChildren 347 | } 348 | 349 | func CheckExtension(path string, extensions []string) bool { 350 | extension := filepath.Ext(path) 351 | for _, item := range extensions { 352 | if item == extension { 353 | return true 354 | } 355 | } 356 | return false 357 | } 358 | 359 | func parseFile(dir, subpath string) *node { 360 | path, err := filepath.Abs(filepath.Join(dir, subpath)) 361 | if err != nil { 362 | log.Fatal(err) 363 | } 364 | 365 | content, err := ioutil.ReadFile(path) 366 | if err != nil { 367 | log.Fatal(err) 368 | } 369 | 370 | // add dependency. 371 | dependencies.addVertices(path) 372 | 373 | root := newNode(TypeRoot, nil) 374 | root.insert(dir, subpath, content) 375 | 376 | for _, t := range []uint8{TypeExtend, TypeDefinition} { 377 | children := root.childrenByType(t) 378 | if len(children) > 1 { 379 | log.Fatalf( 380 | "there should be at most one Extend or Definition in file %s", 381 | path, 382 | ) 383 | } 384 | } 385 | parsedNodes[path] = root 386 | 387 | return root 388 | } 389 | 390 | func parseDir(dir string, extensions []string) { 391 | err := filepath.Walk(dir, func( 392 | path string, info os.FileInfo, err error) error { 393 | 394 | if err != nil { 395 | return err 396 | } 397 | 398 | stat, err := os.Stat(path) 399 | if err != nil { 400 | return err 401 | } 402 | 403 | if !stat.IsDir() && CheckExtension(path, extensions) { 404 | parseFile(dir, path[len(dir):]) 405 | } 406 | return nil 407 | }) 408 | if err != nil { 409 | log.Fatal(err) 410 | } 411 | } 412 | 413 | func rebuild() { 414 | queue := dependencies.sort() 415 | for _, path := range queue { 416 | if _, err := os.Stat(path); err == nil { 417 | continue 418 | } else if os.IsNotExist(err) { 419 | log.Fatal(path, " not found") 420 | } else { 421 | log.Fatal(err) 422 | } 423 | } 424 | 425 | for _, path := range queue { 426 | if node, ok := parsedNodes[path]; !ok { 427 | log.Fatalf("dependency %s not parsed", path) 428 | } else { 429 | node.rebuild() 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | var rootDir string 15 | 16 | const indexHTML = ` 17 | 18 | 19 | 20 | 21 | 22 | <%@ body { %> 23 | <% } %> 24 | 25 | 26 | ` 27 | 28 | const itemHTML = ` 29 |
    30 | 31 | <%== user %> 32 | 33 |
    34 | ` 35 | 36 | const listHTML = ` 37 | <%: func UserList(userList []string, buffer *bytes.Buffer) %> 38 | 39 | <%! 40 | func Add(a, b int) int { 41 | return a + b 42 | } 43 | %> 44 | 45 | <%~ "index.html" %> 46 | 47 | <%@ body { %> 48 | <%# this is note %> 49 | <% for _, user := range userList { %> 50 | <%+ "item.html" %> 51 | <% } %> 52 | <% } %> 53 | ` 54 | 55 | const listToWriterHTML = ` 56 | <%: func UserListToWriter(userList []string, w io.Writer) %> 57 | 58 | <%~ "index.html" %> 59 | 60 | <%@ body { %> 61 | <%# this is note %> 62 | <% for _, user := range userList { %> 63 | <%+ "item.html" %> 64 | <% } %> 65 | <% } %> 66 | ` 67 | 68 | const listToWriterWithResultHTML = ` 69 | <%: func UserListToWriterWithResult(userList []string, w io.Writer) (n int, err error) %> 70 | 71 | <%~ "index.html" %> 72 | 73 | <%@ body { %> 74 | <%# this is note %> 75 | <% for _, user := range userList { %> 76 | <%+ "item.html" %> 77 | <% } %> 78 | <% } %> 79 | ` 80 | 81 | func init() { 82 | if runtime.GOOS != "windows" { 83 | rootDir = "/tmp/gohero" 84 | } else { 85 | rootDir = `C:\tmp\gohero` 86 | } 87 | 88 | _, err := os.Stat(rootDir) 89 | if !os.IsNotExist(err) { 90 | if err = os.RemoveAll(rootDir); err != nil { 91 | log.Panic(err) 92 | } 93 | } 94 | if err = os.Mkdir(rootDir, os.ModePerm); err != nil { 95 | log.Panic(err) 96 | } 97 | 98 | items := []struct { 99 | name string 100 | content string 101 | }{ 102 | {"index.html", indexHTML}, 103 | {"item.html", itemHTML}, 104 | {"list.html", listHTML}, 105 | {"listwriter.html", listToWriterHTML}, 106 | {"listwriterresult.html", listToWriterWithResultHTML}, 107 | } 108 | 109 | for _, item := range items { 110 | err = ioutil.WriteFile( 111 | filepath.Join(rootDir, item.name), 112 | []byte(item.content), 113 | os.ModePerm, 114 | ) 115 | if err != nil { 116 | log.Panic(err) 117 | } 118 | } 119 | } 120 | 121 | func TestNewNode(t *testing.T) { 122 | var n *node 123 | 124 | cases := []struct { 125 | t uint8 126 | chunk []byte 127 | }{ 128 | {TypeCode, []byte{1, 2}}, 129 | {TypeExtend, []byte{3, 4}}, 130 | {TypeEscapedValue, []byte{5, 6}}, 131 | } 132 | 133 | for _, c := range cases { 134 | n = newNode(c.t, c.chunk) 135 | if n.t != c.t || !reflect.DeepEqual(n.chunk.Bytes(), c.chunk) || 136 | n.children == nil || len(n.children) != 0 { 137 | t.Fail() 138 | } 139 | } 140 | } 141 | 142 | func TestSplitByEndBlock(t *testing.T) { 143 | cases := []struct { 144 | in []byte 145 | bs1 []byte 146 | bs2 []byte 147 | }{ 148 | {in: []byte("hello<% } %>world"), bs1: []byte("hello"), bs2: []byte("world")}, 149 | {in: []byte(" a <% } %> b "), bs1: []byte(" a "), bs2: []byte(" b ")}, 150 | } 151 | 152 | for _, c := range cases { 153 | if bs1, bs2 := splitByEndBlock(c.in); !reflect.DeepEqual(c.bs1, bs1) || 154 | !reflect.DeepEqual(c.bs2, bs2) { 155 | t.Fail() 156 | } 157 | } 158 | } 159 | 160 | func buildTree() *node { 161 | root := newNode(TypeRoot, nil) 162 | root.insert(rootDir, "list.html", []byte(listHTML)) 163 | return root 164 | } 165 | 166 | func testList(root *node, t *testing.T) { 167 | var ( 168 | child *node 169 | content string 170 | ) 171 | 172 | child = root.children[0] 173 | content = strings.TrimSpace(child.chunk.String()) 174 | if child.t != TypeDefinition || 175 | content != "func UserList(userList []string, buffer *bytes.Buffer)" { 176 | t.Fail() 177 | } 178 | 179 | child = root.children[1] 180 | content = strings.TrimSpace(child.chunk.String()) 181 | if child.t != TypeImport || content != `func Add(a, b int) int { 182 | return a + b 183 | }` { 184 | t.Fail() 185 | } 186 | 187 | child = root.children[2] 188 | content = strings.TrimSpace(child.chunk.String()) 189 | if child.t != TypeExtend || content != filepath.Join(rootDir, "index.html") { 190 | t.Fail() 191 | } 192 | 193 | child = root.children[3] 194 | content = strings.TrimSpace(child.chunk.String()) 195 | if child.t != TypeBlock || content != "body" || len(child.children) != 4 { 196 | t.Fail() 197 | } 198 | 199 | include := child.children[2] 200 | if include.t != TypeInclude || 201 | include.chunk.String() != filepath.Join(rootDir, "item.html") || 202 | len(include.children) != 0 { 203 | t.Fail() 204 | } 205 | } 206 | 207 | func TestInsert(t *testing.T) { 208 | root := buildTree() 209 | testList(root, t) 210 | } 211 | 212 | func TestChildrenByType(t *testing.T) { 213 | root := buildTree() 214 | 215 | var children []*node 216 | 217 | children = root.childrenByType(TypeHTML) 218 | if len(children) > 0 { 219 | t.Fail() 220 | } 221 | 222 | children = root.childrenByType(TypeDefinition) 223 | content := strings.TrimSpace(children[0].chunk.String()) 224 | if len(children) != 1 || children[0].t != TypeDefinition || 225 | content != "func UserList(userList []string, buffer *bytes.Buffer)" { 226 | t.Fail() 227 | } 228 | } 229 | 230 | func TestFindBlockByName(t *testing.T) { 231 | root := buildTree() 232 | 233 | cases := []struct { 234 | name string 235 | existed bool 236 | }{ 237 | {"head", false}, 238 | {"body", true}, 239 | } 240 | 241 | for _, c := range cases { 242 | if block := root.findBlockByName(c.name); !(block != nil == c.existed) { 243 | t.Fail() 244 | } 245 | } 246 | } 247 | 248 | func TestParseFile(t *testing.T) { 249 | root := parseFile(rootDir, "list.html") 250 | testList(root, t) 251 | 252 | if len(dependencies.vertices) != 3 { 253 | t.Fail() 254 | } 255 | 256 | pathIndex := filepath.Join(rootDir, "index.html") 257 | pathItem := filepath.Join(rootDir, "item.html") 258 | pathList := filepath.Join(rootDir, "list.html") 259 | 260 | vertices := map[string]struct{}{ 261 | pathIndex: {}, 262 | pathItem: {}, 263 | pathList: {}, 264 | } 265 | 266 | if !reflect.DeepEqual(vertices, dependencies.vertices) { 267 | t.Fail() 268 | } 269 | 270 | graph := map[string]map[string]struct{}{ 271 | pathIndex: { 272 | pathList: {}, 273 | }, 274 | pathItem: { 275 | pathList: {}, 276 | }, 277 | } 278 | 279 | if !reflect.DeepEqual(graph, dependencies.graph) { 280 | t.Fail() 281 | } 282 | } 283 | 284 | func testRebuild(root *node, t *testing.T) { 285 | if len(root.children) != 5 { 286 | t.Fail() 287 | } 288 | 289 | var ( 290 | child *node 291 | content string 292 | ) 293 | 294 | child = root.children[0] 295 | content = strings.TrimSpace(child.chunk.String()) 296 | if child.t != TypeImport || content != `func Add(a, b int) int { 297 | return a + b 298 | }` { 299 | t.Fail() 300 | } 301 | 302 | child = root.children[1] 303 | content = strings.TrimSpace(child.chunk.String()) 304 | if child.t != TypeDefinition || 305 | content != "func UserList(userList []string, buffer *bytes.Buffer)" { 306 | t.Fail() 307 | } 308 | 309 | child = root.children[2] 310 | content = strings.TrimSpace(child.chunk.String()) 311 | if child.t != TypeHTML || 312 | content != ` 313 | 314 | 315 | 316 | ` { 317 | t.Fail() 318 | } 319 | 320 | child = root.children[3] 321 | content = strings.TrimSpace(child.chunk.String()) 322 | if child.t != TypeBlock || content != "body" || len(child.children) != 4 { 323 | t.Fail() 324 | } 325 | 326 | include := child.children[2] 327 | if include.t != TypeInclude || 328 | include.chunk.String() != filepath.Join(rootDir, "item.html") || 329 | len(include.children) != 5 { 330 | t.Fail() 331 | } 332 | 333 | child = root.children[4] 334 | content = strings.TrimSpace(child.chunk.String()) 335 | if child.t != TypeHTML || content != ` 336 | ` { 337 | t.Fail() 338 | } 339 | } 340 | 341 | func TestRebuild(t *testing.T) { 342 | paths := []string{ 343 | "index.html", 344 | "item.html", 345 | "list.html", 346 | } 347 | 348 | for _, p := range paths { 349 | parsedNodes[filepath.Join(rootDir, p)] = parseFile(rootDir, p) 350 | } 351 | 352 | root := parsedNodes[filepath.Join(rootDir, "list.html")] 353 | root.rebuild() 354 | 355 | testRebuild(root, t) 356 | } 357 | 358 | func TestParseDir(t *testing.T) { 359 | dependencies.graph = make(map[string]map[string]struct{}) 360 | dependencies.vertices = make(map[string]struct{}) 361 | 362 | parseDir(rootDir) 363 | 364 | root := parsedNodes[filepath.Join(rootDir, "list.html")] 365 | testRebuild(root, t) 366 | } 367 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | // sort implements topological sort. 4 | type sort struct { 5 | vertices map[string]struct{} 6 | graph map[string]map[string]struct{} 7 | v map[string]int 8 | } 9 | 10 | func newSort() *sort { 11 | return &sort{ 12 | vertices: make(map[string]struct{}), 13 | graph: make(map[string]map[string]struct{}), 14 | v: make(map[string]int), 15 | } 16 | } 17 | 18 | func (s *sort) addVertices(vertices ...string) { 19 | for _, vertex := range vertices { 20 | s.vertices[vertex] = struct{}{} 21 | } 22 | } 23 | 24 | func (s *sort) addEdge(from, to string) { 25 | if _, ok := s.graph[from]; !ok { 26 | s.graph[from] = map[string]struct{}{ 27 | to: {}, 28 | } 29 | } else { 30 | s.graph[from][to] = struct{}{} 31 | } 32 | } 33 | 34 | func (s *sort) collect(queue *[]string) { 35 | for vertex, count := range s.v { 36 | if count == 0 { 37 | *queue = append(*queue, vertex) 38 | delete(s.v, vertex) 39 | } 40 | } 41 | } 42 | 43 | // sort returns sorted string list. 44 | func (s *sort) sort() []string { 45 | for vertex := range s.vertices { 46 | s.v[vertex] = 0 47 | } 48 | 49 | for _, tos := range s.graph { 50 | for to := range tos { 51 | s.v[to]++ 52 | } 53 | } 54 | 55 | r := make([]string, 0, len(s.vertices)) 56 | 57 | queue := make([]string, 0, len(s.vertices)) 58 | s.collect(&queue) 59 | 60 | for len(queue) > 0 { 61 | r = append(r, queue[0]) 62 | if tos, ok := s.graph[queue[0]]; ok { 63 | for to := range tos { 64 | s.v[to]-- 65 | } 66 | } 67 | 68 | s.collect(&queue) 69 | 70 | delete(s.graph, queue[0]) 71 | queue = queue[1:] 72 | } 73 | 74 | if len(s.v) > 0 { 75 | panic("import or include cycle") 76 | } 77 | 78 | return r 79 | } 80 | -------------------------------------------------------------------------------- /sort_test.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAddVertices(t *testing.T) { 9 | sort := newSort() 10 | 11 | vertices := []string{"a", "b", "c", "d", "e"} 12 | for _, vertex := range vertices { 13 | sort.addVertices(vertex) 14 | } 15 | 16 | if len(sort.vertices) != len(vertices) { 17 | t.Fail() 18 | } 19 | 20 | for _, vertex := range vertices { 21 | if _, ok := sort.vertices[vertex]; !ok { 22 | t.Fail() 23 | } 24 | } 25 | } 26 | 27 | func TestAddEdge(t *testing.T) { 28 | sort := newSort() 29 | 30 | edges := map[string][]string{ 31 | "a": {"1"}, 32 | "b": {"2", "3"}, 33 | "c": {"4", "5", "6"}, 34 | } 35 | 36 | // init graph. 37 | for from, tos := range edges { 38 | for _, to := range tos { 39 | sort.addEdge(from, to) 40 | } 41 | } 42 | 43 | if len(sort.graph) != len(edges) { 44 | t.Fail() 45 | } 46 | 47 | for from, tos := range edges { 48 | if v, ok := sort.graph[from]; !ok || len(v) != len(tos) { 49 | t.Fail() 50 | } 51 | 52 | for _, to := range tos { 53 | if _, ok := sort.graph[from][to]; !ok { 54 | t.Fail() 55 | } 56 | } 57 | } 58 | } 59 | 60 | func TestCollect(t *testing.T) { 61 | sort := &sort{ 62 | v: map[string]int{ 63 | "a": 0, 64 | "b": 1, 65 | "c": 2, 66 | }, 67 | } 68 | 69 | var queue []string 70 | sort.collect(&queue) 71 | 72 | if !reflect.DeepEqual(queue, []string{"a"}) || 73 | !reflect.DeepEqual(sort.v, map[string]int{"b": 1, "c": 2}) { 74 | t.Fail() 75 | } 76 | } 77 | 78 | func TestSort(t *testing.T) { 79 | sort := newSort() 80 | 81 | vertices := []string{"a", "b", "c", "d", "e"} 82 | for _, vertex := range vertices { 83 | sort.addVertices(vertex) 84 | } 85 | 86 | edges := map[string][]string{ 87 | "a": {"b"}, 88 | "b": {"c"}, 89 | "c": {"d"}, 90 | "d": {"e"}, 91 | } 92 | 93 | for from, tos := range edges { 94 | for _, to := range tos { 95 | sort.addEdge(from, to) 96 | } 97 | } 98 | 99 | if !reflect.DeepEqual(sort.sort(), vertices) { 100 | t.Fail() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | escapedKeys = []byte{'&', '\'', '<', '>', '"'} 14 | escapedValues = []string{"&", "'", "<", ">", """} 15 | ) 16 | 17 | // EscapeHTML escapes the html and then put it to the buffer. 18 | func EscapeHTML(html string, buffer *bytes.Buffer) { 19 | var i, j, k int 20 | 21 | for i < len(html) { 22 | for j = i; j < len(html); j++ { 23 | k = bytes.IndexByte(escapedKeys, html[j]) 24 | if k != -1 { 25 | break 26 | } 27 | } 28 | 29 | buffer.WriteString(html[i:j]) 30 | if k != -1 { 31 | buffer.WriteString(escapedValues[k]) 32 | } 33 | i = j + 1 34 | } 35 | } 36 | 37 | // FormatUint formats uint to string and put it to the buffer. 38 | // It's part of go source: 39 | // https://github.com/golang/go/blob/master/src/strconv/itoa.go#L60 40 | func FormatUint(u uint64, buffer *bytes.Buffer) { 41 | var a [64 + 1]byte 42 | i := len(a) 43 | 44 | if ^uintptr(0)>>32 == 0 { 45 | for u > uint64(^uintptr(0)) { 46 | q := u / 1e9 47 | us := uintptr(u - q*1e9) 48 | for j := 9; j > 0; j-- { 49 | i-- 50 | qs := us / 10 51 | a[i] = byte(us - qs*10 + '0') 52 | us = qs 53 | } 54 | u = q 55 | } 56 | } 57 | 58 | us := uintptr(u) 59 | for us >= 10 { 60 | i-- 61 | q := us / 10 62 | a[i] = byte(us - q*10 + '0') 63 | us = q 64 | } 65 | 66 | i-- 67 | a[i] = byte(us + '0') 68 | buffer.Write(a[i:]) 69 | } 70 | 71 | // FormatInt format int to string and then put the result to the buffer. 72 | func FormatInt(i int64, buffer *bytes.Buffer) { 73 | if i < 0 { 74 | buffer.WriteByte('-') 75 | i = -i 76 | } 77 | FormatUint(uint64(i), buffer) 78 | } 79 | 80 | // FormatFloat format float64 to string and then put the result to the buffer. 81 | func FormatFloat(f float64, buffer *bytes.Buffer) { 82 | buffer.WriteString(strconv.FormatFloat(f, 'f', -1, 64)) 83 | } 84 | 85 | // FormatBool format bool to string and then put the result to the buffer. 86 | func FormatBool(b bool, buffer *bytes.Buffer) { 87 | if b { 88 | buffer.WriteString("true") 89 | return 90 | } 91 | buffer.WriteString("false") 92 | } 93 | 94 | // execCommand wraps exec.Command 95 | func execCommand(command string) { 96 | parts := strings.Split(command, " ") 97 | if len(parts) == 0 { 98 | return 99 | } 100 | 101 | cmd := exec.Command(parts[0], parts[1:]...) 102 | cmd.Stderr = os.Stderr 103 | 104 | err := cmd.Run() 105 | if err != nil { 106 | fmt.Println(err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package hero 2 | 3 | import ( 4 | "bytes" 5 | "runtime" 6 | "testing" 7 | ) 8 | 9 | func TestExecCommand(t *testing.T) { 10 | // test for panic 11 | if runtime.GOOS != "windows" { 12 | execCommand("") 13 | execCommand("ls") 14 | } else { 15 | execCommand("dir") 16 | } 17 | } 18 | 19 | func TestFormatUint(t *testing.T) { 20 | cases := []struct { 21 | in uint64 22 | out string 23 | }{ 24 | {in: 0, out: "0"}, 25 | {in: 1, out: "1"}, 26 | {in: 100, out: "100"}, 27 | {in: 101, out: "101"}, 28 | } 29 | 30 | buffer := new(bytes.Buffer) 31 | 32 | for _, c := range cases { 33 | FormatUint(c.in, buffer) 34 | if buffer.String() != c.out { 35 | t.Fail() 36 | } 37 | buffer.Reset() 38 | } 39 | } 40 | --------------------------------------------------------------------------------