├── .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 | [](https://godoc.org/github.com/shiyanhui/hero)
7 | [](https://goreportcard.com/report/github.com/shiyanhui/hero)
8 | [](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 |
98 | <%+ "user.html" %>
99 |
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 | [](https://godoc.org/github.com/shiyanhui/hero)
6 | [](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 |
86 | <%+ "user.html" %>
87 |
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 | // [](https://godoc.org/github.com/shiyanhui/hero)
7 | // [](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 | //
87 | // <%+ "user.html" %>
88 | //
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 |
8 | <%+ "user.html" %>
9 |
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 |
24 | `)
25 | buffer.WriteString(`-
26 | `)
27 | hero.EscapeHTML(user, buffer)
28 | buffer.WriteString(`
29 |
30 | `)
31 |
32 | buffer.WriteString(`
33 |
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 |
8 | <%+ "user.html" %>
9 |
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 |
26 | `)
27 | _buffer.WriteString(`-
28 | `)
29 | hero.EscapeHTML(user, _buffer)
30 | _buffer.WriteString(`
31 |
32 | `)
33 |
34 | _buffer.WriteString(`
35 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------