├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── fixtures
└── base
│ ├── admin
│ ├── body.html
│ └── js.tmpl
│ ├── foot.html
│ └── layout.html
├── html.go
├── template.go
├── template_test.go
├── text.go
└── utils.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | *. text eol=lf
3 | .* text eol=lf
4 | *.go text eol=lf
5 | *.html text eol=lf
6 | *.tmpl text eol=lf
7 | *.yml text eol=lf
8 | *.toml text eol=lf
9 | *.md text eol=lf
10 | *.json text eol=lf
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 | pkg
10 |
11 | # Architecture specific extensions/prefixes
12 | *.[568vq]
13 | [568vq].out
14 |
15 | *.cgo1.go
16 | *.cgo2.c
17 | _cgo_defun.c
18 | _cgo_gotypes.go
19 | _cgo_export.*
20 |
21 | _testmain.go
22 |
23 | *.exe
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright (c) 2013, YU HengChun
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification,
6 | are permitted provided that the following conditions are met:
7 |
8 | Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | Redistributions in binary form must reproduce the above copyright notice, this
12 | list of conditions and the following disclaimer in the documentation and/or
13 | other materials provided with the distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Template
2 | ========
3 |
4 | 基于官方 `text/template` 和 `html/template` 的模板引擎.
5 | Template 通过几种惯用方式组合, 为模板提供简洁的使用方式.
6 |
7 | 特性
8 | ====
9 |
10 | * 模板名仿效 URI 格式, 使用全路径名称命名.
11 | * 模板名以 ".html" 结尾当作 HTML 模板处理, 否则当作 TEXT 模板处理.
12 | * 模板源码可使用相对路径名指示目标模板.
13 | * 引入 RootDir 限制模板文件根目录.
14 | * 内置 import 函数支持变量名表示模板名.
15 |
16 | 使用
17 | ====
18 |
19 | 以源码 fixtures/base 目录下的文件为例:
20 |
21 | ```
22 | \---base
23 | | foot.html
24 | | layout.html 单独列出
25 | |
26 | \---admin
27 | body.html
28 | js.tmpl
29 | ```
30 |
31 | layout.html 内容, 注意 import 支持变量, 支持目标模板名采用相对路径:
32 |
33 | ```html
34 |
35 |
36 |
37 | {{import .js}}
38 |
39 |
40 | {{import .body .}}
41 |
42 | {{template "foot.html"}}
43 |
44 | ```
45 |
46 | GoLang 代码:
47 |
48 | ```go
49 | package main
50 |
51 | import (
52 | "github.com/achun/template"
53 | "os"
54 | )
55 |
56 | var data = map[string]interface{}{
57 | "title": `>title`,
58 | "body": `/admin/body.html`,
59 | "js": `/admin/js.tmpl`,
60 | "href": ">>>",
61 | "name": "admin",
62 | }
63 |
64 | func main() {
65 | pwd, _ := os.Getwd()
66 | t, err := template.New("./fixtures/base/layout.html")
67 | t.Walk(pwd+`/fixtures/base`, ".html.tmpl")
68 | t.Execute(os.Stdout, data)
69 | }
70 | ```
71 |
72 | 输出:
73 |
74 | ```html
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | ```
86 |
87 | 内部实现
88 | ========
89 |
90 | 通过重写 *parse.Tree 中的 `template` 用 `import` 函数替换. 并且重新计算目标路径为绝对路径. 最终形成的 `import` 定义为:
91 |
92 | ```go
93 | func(from, name string, data ...interface{}) (template.HTML, error)
94 | ```
95 |
96 | * from 为发起调用的模板名称, form 是自动计算出的, 使用者不能定义.
97 | * name 为模板模板名称
98 | * data 为用户数据
99 |
100 | name 支持变量, 此变量有可能采用相对路径, form 为计算绝对路径提供了参照.
101 | 而 rootdir 保证所有的相对路径都可以计算出绝对路径.
102 |
103 | License
104 | =======
105 | template is licensed under the BSD
--------------------------------------------------------------------------------
/fixtures/base/admin/body.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fixtures/base/admin/js.tmpl:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fixtures/base/foot.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fixtures/base/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{import .js}}
5 |
6 |
7 | {{import .body .}}
8 |
9 | {{template "foot.html"}}
10 |
--------------------------------------------------------------------------------
/html.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "html/template"
5 | "io"
6 | "text/template/parse"
7 | )
8 |
9 | type htmlTemp struct {
10 | *template.Template
11 | }
12 |
13 | /**
14 | newHtml 新建基于 "html/template" 的共享模板.
15 | */
16 | func newHtml() executor {
17 | return htmlTemp{
18 | Template: template.New(shareName),
19 | }
20 | }
21 |
22 | func (t htmlTemp) AddParseTree(tree *parse.Tree) (executor, error) {
23 |
24 | nt, err := t.Template.New(tree.Name).AddParseTree(tree.Name, tree)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | if t.Template.Tree == nil {
30 | t.Template = nt
31 | return t, nil
32 | }
33 |
34 | return htmlTemp{
35 | Template: nt,
36 | }, nil
37 | }
38 |
39 | func (t htmlTemp) Execute(
40 | p *Template, wr io.Writer, data interface{}) error {
41 | return t.Template.Execute(wr, data)
42 | }
43 |
44 | func (t htmlTemp) Funcs(funcMap FuncMap) {
45 | t.Template.Funcs(template.FuncMap(funcMap))
46 | }
47 |
48 | func (t htmlTemp) Lookup(name string) executor {
49 |
50 | nt := t.Template.Lookup(name)
51 | if nt == nil {
52 | return nil
53 | }
54 |
55 | return htmlTemp{
56 | Template: nt,
57 | }
58 | }
59 |
60 | func (t htmlTemp) Kind() Kind {
61 | return HTML
62 | }
63 |
64 | func (t htmlTemp) Tree() *parse.Tree {
65 | return t.Template.Tree
66 | }
67 |
--------------------------------------------------------------------------------
/template.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "html/template"
8 | "io"
9 | "os"
10 | "path"
11 | "path/filepath"
12 | "strings"
13 | "text/template/parse"
14 | )
15 |
16 | var (
17 | errUnavailable = errors.New("template: unavailable action.")
18 | errUnimplemented = errors.New("template: unimplemented action.")
19 | errImport = errors.New("template: unimplemented import.")
20 | errMuseBeNamed = errors.New("template: must be named before.")
21 | errNeverShow = errors.New("template: never show")
22 | )
23 |
24 | var (
25 | // 占位用
26 | placeholderFuncs = FuncMap{
27 | "import": func(from, name string, data ...interface{}) (template.HTML, error) {
28 | return "", nil
29 | },
30 | }
31 | )
32 |
33 | /**
34 | 模板风格
35 | */
36 | type Kind uint8
37 |
38 | const (
39 | INVALID Kind = iota // 无效模板
40 | TEXT // 文本模板, 使用 "text/template" 进行处理.
41 | HTML // html模板, 使用 "html/template" 进行处理.
42 | )
43 |
44 | func musetBeOverWriteThisMethod() {
45 | panic("must be overwrite this method.")
46 | }
47 | func unExpected() {
48 | panic("Unexpected")
49 | }
50 |
51 | type executor interface {
52 | AddParseTree(tree *parse.Tree) (executor, error)
53 | Execute(p *Template, wr io.Writer, data interface{}) error
54 | Funcs(funcMap FuncMap)
55 | Lookup(name string) executor
56 | Kind() Kind
57 | Name() string
58 | Tree() *parse.Tree
59 | }
60 |
61 | /**
62 | kindTree 对 parse.Tree 进行包装, 包含 Kind 信息.
63 | */
64 | type kindTree struct {
65 | *parse.Tree
66 | Kind
67 | }
68 |
69 | /**
70 | Copy 返回一份 kindTree 的拷贝.
71 | */
72 | func (t kindTree) Copy() kindTree {
73 |
74 | var tree *parse.Tree
75 | if t.Tree != nil {
76 | tree = t.Tree.Copy()
77 | }
78 |
79 | return kindTree{
80 | Tree: tree,
81 | Kind: t.Kind,
82 | }
83 | }
84 |
85 | /**
86 | Dir 返回 ParseName 所属的目录.
87 | */
88 | func (t kindTree) Dir() string {
89 | if t.Tree == nil {
90 | return ""
91 | }
92 | return path.Dir(t.Tree.ParseName)
93 | }
94 |
95 | /**
96 | IsValid 返回 t 是否有效.
97 | */
98 | func (t kindTree) IsValid() bool {
99 | return (t.Kind == TEXT || t.Kind == HTML) &&
100 | t.Tree != nil && t.Tree.Name != "" && t.Tree.ParseName != ""
101 | }
102 |
103 | /**
104 | Name 返回 t.Tree.Name, 如果 t.Tree 为 nil, 返回 "".
105 | */
106 | func (t kindTree) Name() string {
107 | if t.Tree == nil {
108 | return ""
109 | }
110 | return t.Tree.Name
111 | }
112 |
113 | /**
114 | ParseName 返回 t.Tree.ParseName, 如果 t.Tree 为 nil, 返回 "".
115 | */
116 | func (t kindTree) ParseName() string {
117 | if t.Tree == nil {
118 | return ""
119 | }
120 | return t.Tree.ParseName
121 | }
122 |
123 | /**
124 | base 维护 Template 的基础数据. 包括: rootdir, Tree, FuncMap.
125 | 注意 Tree, FuncMap 都是用了 map 做容器, 并发读写是不安全的.
126 | */
127 | type base struct {
128 | rootdir string
129 | trees map[string]kindTree
130 | funcs FuncMap
131 | }
132 |
133 | /**
134 | newBase 新建 base 对象.
135 | 参数:
136 | rootdir 是已经处理好的 uri 格式.
137 | */
138 | func newBase(rootdir string) *base {
139 | c := &base{
140 | rootdir: rootdir,
141 | trees: make(map[string]kindTree),
142 | funcs: make(FuncMap),
143 | }
144 | return c
145 | }
146 |
147 | /**
148 | AddTree 增加 Tree.
149 | Tree 是已经处理好的, name 要有 rootdir 前缀.
150 | 返回:
151 | 失败返回错误信息, 成功返回 nil.
152 | */
153 | func (b *base) AddTree(tree kindTree) error {
154 |
155 | if !tree.IsValid() {
156 | return fmt.Errorf("template: invalid Tree from base.AddTree.")
157 | }
158 |
159 | if len(b.rootdir) != 0 {
160 | if len(tree.Tree.Name) < len(b.rootdir) ||
161 | len(tree.Tree.ParseName) < len(b.rootdir) ||
162 | tree.Tree.Name[:len(b.rootdir)] != b.rootdir ||
163 | tree.Tree.ParseName[:len(b.rootdir)] != b.rootdir ||
164 | tree.Tree.Name[len(b.rootdir)] != '/' ||
165 | tree.Tree.ParseName[len(b.rootdir)] != '/' {
166 |
167 | return fmt.Errorf(
168 | "template: %q not under %q.", tree.Name, b.rootdir)
169 | }
170 | }
171 |
172 | if _, exist := b.trees[tree.Tree.Name]; exist {
173 | return fmt.Errorf(
174 | "template: redefinition of %q", tree.Tree.Name)
175 | }
176 |
177 | b.trees[tree.Tree.Name] = tree
178 | return nil
179 | }
180 |
181 | /**
182 | Copy 返回一份 *base 的拷贝.
183 | 通常, 如果不需要即时使用 AddTree, Funcs 无需使用 Copy,
184 | 如果需要, 应保留一份 base 作为原本专用于 Copy.
185 | */
186 | func (b *base) Copy() *base {
187 |
188 | trees := make(map[string]kindTree)
189 |
190 | for name, tree := range b.trees {
191 | trees[name] = tree.Copy()
192 | }
193 |
194 | funcs := make(map[string]interface{}, len(b.funcs))
195 | for k, a := range b.funcs {
196 | funcs[k] = a
197 | }
198 |
199 | return &base{
200 | rootdir: b.rootdir,
201 | trees: trees,
202 | funcs: funcs,
203 | }
204 | }
205 |
206 | /**
207 | Funcs 设置自定义 FuncMap.
208 | */
209 | func (b *base) Funcs(funcMap FuncMap) {
210 | for k, i := range funcMap {
211 | b.funcs[k] = i
212 | }
213 | }
214 |
215 | /**
216 | Lookup 返回 name 对应的 kindTree. 需要使用者判断 kindTree 是否有效.
217 | */
218 | func (b *base) Lookup(name string) kindTree {
219 | return b.trees[name]
220 | }
221 |
222 | /**
223 | RootDir 返回 rootdir.
224 | */
225 | func (b *base) RootDir() string {
226 | return b.rootdir
227 | }
228 |
229 | /**
230 | Template.
231 | */
232 | type Template struct {
233 | base *base
234 | executor // 当前执行器
235 | text executor // text 执行器
236 | html executor // html 执行器
237 | leftDelim string
238 | rightDelim string
239 | }
240 |
241 | func (t *Template) wrap(exec executor) *Template {
242 | if exec == nil || exec.Kind() == INVALID {
243 | return nil
244 | }
245 | nt := &Template{
246 | base: t.base,
247 | text: t.text,
248 | html: t.html,
249 | leftDelim: t.leftDelim,
250 | rightDelim: t.rightDelim,
251 | }
252 | switch exec.Kind() {
253 | default:
254 | return nil
255 | case TEXT:
256 | nt.executor = exec
257 | case HTML:
258 | nt.executor = exec
259 | }
260 | return t
261 | }
262 |
263 | var dataNil = []interface{}{nil}
264 |
265 | func (t *Template) initFuncs() *Template {
266 |
267 | rootdir := t.RootDir()
268 |
269 | funcs := map[string]interface{}{
270 | "import": func(from, name string,
271 | data ...interface{}) (template.HTML, error) {
272 |
273 | var (
274 | buf bytes.Buffer
275 | exec executor
276 | )
277 |
278 | // 先假设为绝对路径, 找不到, 再求相对路径.
279 | exec = t.lookup(name)
280 | if exec == nil {
281 | exec = t.lookup(relToURI(
282 | rootdir, t.base.trees[from].Dir(), cleanURI(name)))
283 | }
284 |
285 | if exec == nil {
286 | return "", fmt.Errorf("template: %q is undefined", name)
287 | }
288 |
289 | if len(data) == 0 {
290 | data = dataNil
291 | }
292 |
293 | err := exec.Execute(t, &buf, data[0])
294 | if err != nil {
295 | return "", err
296 | }
297 | return template.HTML(buf.String()), nil
298 | },
299 | }
300 |
301 | t.text.Funcs(funcs)
302 | t.html.Funcs(funcs)
303 |
304 | return t
305 | }
306 |
307 | /**
308 | New 基于资源路径 uri 新建一个 Template.
309 | 先对 uri 进行绝对路径计算, 计算出 rootdir 和是否要加载文件.
310 |
311 | 参数:
312 | uri 资源路径可以是目录或者文件, 无扩展名当作目录, 否则当作文件.
313 | 如果 uri 为空, 用 os.Getwd() 获取目录.
314 | 如果 uri 以 `./` 或 `.\` 开头自动加当前路径, 否则当作绝对路径.
315 | 如果 uri 含扩展当作模板文件, 使用 ParseFiles 解析.
316 | uri 所指的目录被设置为 rootdir, 后续载入的文件被限制在此目录下.
317 |
318 | funcMap 可选自定义 FuncMap.
319 | 当 uri 为文件时, funcMap 参数可保障正确解析模板中的函数.
320 | 返回:
321 | 模板实例和发生的错误.
322 | */
323 | func New(uri string, funcMap ...FuncMap) (*Template, error) {
324 |
325 | var err error
326 |
327 | if uri == "" {
328 | uri, err = os.Getwd()
329 | if err != nil {
330 | return nil, err
331 | }
332 | } else if len(uri) > 1 && (uri[:2] == `./` ||
333 | uri[:2] == `.\`) {
334 |
335 | dir, err := os.Getwd()
336 | if err != nil {
337 | return nil, err
338 | }
339 | uri = dir + `/` + uri
340 | }
341 |
342 | rootdir := cleanURI(uri)
343 | if rootdir == "" {
344 | return nil, fmt.Errorf("template: invalid uri: %q", uri)
345 | }
346 |
347 | ext := path.Ext(rootdir)
348 |
349 | t := &Template{
350 | text: newText(),
351 | html: newHtml(),
352 | }
353 |
354 | if ext == "" {
355 | t.base = newBase(rootdir)
356 | } else {
357 | t.base = newBase(path.Dir(rootdir))
358 | }
359 |
360 | t.initFuncs() // init import
361 |
362 | for _, funcs := range funcMap {
363 | t.Funcs(funcs)
364 | }
365 |
366 | if ext != "" {
367 | err = t.ParseFiles(rootdir)
368 | }
369 |
370 | if err != nil {
371 | return nil, err
372 | }
373 | return t, nil
374 | }
375 |
376 | /**
377 | AddParseTree 添加 tree.
378 | 参数:
379 | kind 值为TEXT 或 HTML, 指示 tree 采用何种风格执行.
380 | tree 是已经处理好的, 且 tree.Name 对应绝对路径的模板名称.
381 | 返回:
382 | 如果 tree 符合要求, 返回 tree 对应的 *Template.
383 | 否则返回 nil 和错误.
384 |
385 | 细节:
386 | 事实上 ParseFiles, ParseGlob 都调用了 AddParseTree.
387 | 如果 t 没有对应的执行模板, 自动绑定第一个 Tree 对应的模板.
388 | */
389 | func (t *Template) AddParseTree(
390 | kind Kind, tree *parse.Tree) (*Template, error) {
391 |
392 | var e executor
393 |
394 | err := t.base.AddTree(kindTree{
395 | Tree: tree,
396 | Kind: kind,
397 | })
398 |
399 | if err == nil {
400 | if kind == TEXT {
401 | e, err = t.text.AddParseTree(tree)
402 | } else {
403 | e, err = t.html.AddParseTree(tree)
404 | }
405 | }
406 |
407 | if err != nil {
408 | delete(t.base.trees, tree.Name)
409 | return nil, err
410 | }
411 |
412 | if t.executor == nil {
413 | t.executor = e
414 | return t, nil
415 | }
416 |
417 | return t.wrap(e), nil
418 | }
419 |
420 | /**
421 | Copy 返回一份 *Template 的拷贝. 这是真正的拷贝.
422 | 非并发安全, 如果需要 Copy 功能, 应保留一份母本专用于 Copy.
423 | 提示:
424 | Copy 会重建 FuncMap 中的 "import" 函数.
425 | */
426 | func (t *Template) Copy() (*Template, error) {
427 |
428 | nt := &Template{
429 | base: t.base.Copy(),
430 | text: newText(),
431 | html: newHtml(),
432 | leftDelim: t.leftDelim,
433 | rightDelim: t.rightDelim,
434 | }
435 |
436 | // FuncMap
437 | nt.initFuncs()
438 | nt.text.Funcs(nt.base.funcs)
439 | nt.html.Funcs(nt.base.funcs)
440 |
441 | // 保持当前 executor 类型
442 | tree := t.base.Lookup(t.executor.Name())
443 | name := ""
444 | switch tree.Kind {
445 | default:
446 | nt.executor = nt.text
447 |
448 | case TEXT:
449 | name = tree.Tree.Name
450 | nt.executor = nt.text
451 | nt.executor.AddParseTree(tree.Tree)
452 |
453 | case HTML:
454 | name = tree.Tree.Name
455 | nt.executor = nt.html
456 | nt.executor.AddParseTree(tree.Tree)
457 | }
458 |
459 | // 重建环境
460 | for k, tree := range t.base.trees {
461 | if k == name {
462 | continue
463 | }
464 | switch tree.Kind {
465 | case TEXT:
466 | nt.text.AddParseTree(tree.Tree)
467 | case HTML:
468 | nt.html.AddParseTree(tree.Tree)
469 | }
470 | }
471 |
472 | return nt, nil
473 | }
474 |
475 | /**
476 | Execute 执行模板, 把结果写入 wr.
477 | */
478 | func (t *Template) Execute(
479 | wr io.Writer, data interface{}) error {
480 |
481 | return t.executor.Execute(t, wr, data)
482 | }
483 |
484 | /**
485 | ExecuteTemplate 执行 name 对应的模板, 把结果写入 wr.
486 | 此方法先调用 Lookup 获取 name 对应的模板, 然后执行它.
487 | */
488 | func (t *Template) ExecuteTemplate(
489 | wr io.Writer, name string, data interface{}) error {
490 |
491 | a := t.Lookup(name)
492 | if a == nil {
493 | return fmt.Errorf("template: %q is undefined", name)
494 | }
495 | return a.Execute(wr, data)
496 | }
497 |
498 | /**
499 | Delims 设置模板定界符. 返回 t.
500 | */
501 | func (t *Template) Delims(left, right string) *Template {
502 | t.leftDelim, t.rightDelim = left, right
503 | return t
504 | }
505 |
506 | /**
507 | Dir 返回 t 所在目录绝对路径. slash 分割. 尾部没有 slash.
508 | */
509 | func (t *Template) Dir() string {
510 | ns := t.executor.Name()
511 | if ns == shareName {
512 | return t.base.rootdir
513 | }
514 |
515 | if path.Ext(ns) != "" {
516 | return path.Dir(ns)
517 | }
518 | return path.Dir(path.Dir(ns))
519 | }
520 |
521 | /**
522 | Funcs 给模板绑定自定义 FuncMap.
523 | 参数:
524 | funcMap 设定一次, 在所有相关模板中都会生效.
525 | 返回: t
526 | */
527 | func (t *Template) Funcs(funcMap FuncMap) *Template {
528 | t.base.Funcs(funcMap)
529 | t.text.Funcs(funcMap)
530 | t.html.Funcs(funcMap)
531 | return t
532 | }
533 |
534 | /**
535 | Lookup 取出 name 对应的 *Template.
536 | 参数:
537 | name 模板名, 相对路径. 如果以 "/" 开头表示从 rootdir 开始,
538 | 否则从 t.Dir() 所在目录开始.
539 | 返回:
540 | 返回 name 对应模板, 如果 name 为空或者未找到对应模板, 返回 nil.
541 | */
542 | func (t *Template) Lookup(name string) *Template {
543 |
544 | // 计算绝对路径
545 | name = relToURI(t.base.rootdir, t.Dir(), cleanURI(name))
546 | if name == "" {
547 | return nil
548 | }
549 |
550 | return t.wrap(t.lookup(name))
551 | }
552 |
553 | func (t *Template) lookup(name string) executor {
554 |
555 | var exec, first, second executor
556 |
557 | ext := path.Ext(name)
558 |
559 | // 内嵌模板有可能没有扩展名
560 | if ext == ".html" || t.Kind() == HTML {
561 | first, second = t.html, t.text
562 | } else {
563 | first, second = t.text, t.html
564 | }
565 |
566 | exec = first.Lookup(name)
567 | if exec == nil {
568 | exec = second.Lookup(name)
569 | }
570 |
571 | return exec
572 | }
573 |
574 | /**
575 | Name 返回 uri 风格的模板名, 事实是模板对应的绝对路径.
576 | 如果为空表示模板无效.
577 | */
578 | func (t *Template) Name() string {
579 |
580 | if t.executor == nil {
581 | return ""
582 | }
583 | s := t.executor.Name()
584 | if s == shareName {
585 | return ""
586 | }
587 | return s
588 | }
589 |
590 | /**
591 | ParseFiles 解析多个模板文件. 自动跳过重复的文件.
592 | 参数:
593 | filename 模板文件, 可使用相对路径或绝对路径.
594 | 返回:
595 | 是否有错误发生.
596 | */
597 | func (t *Template) ParseFiles(
598 | filename ...string) error {
599 |
600 | var name string
601 | rootdir := t.RootDir()
602 |
603 | for i := 0; i < len(filename); i++ {
604 | name = absToURI(rootdir, filename[i])
605 | if name == "" || path.Ext(name) == "" {
606 | return fmt.Errorf(
607 | "template: invalid filename: %q", filename[i])
608 | }
609 |
610 | filename[i] = name
611 | }
612 |
613 | err := parseFiles(t, filename...)
614 |
615 | if err != nil {
616 | return err
617 | }
618 |
619 | return nil
620 | }
621 |
622 | /**
623 | Parse 解析模板源代码 text, 并以 name 命名解析后的模板.
624 | 参数:
625 | name 模板名字, 相对于 rootdir 的绝对路径名.
626 | text 待解析的模板源代码.
627 | 返回:
628 | 解析后的模板和发生的错误.
629 | */
630 | func (t *Template) Parse(name, text string) (*Template, error) {
631 |
632 | ns := relToURI(t.RootDir(), t.Dir(), cleanURI(name))
633 |
634 | if ns == "" {
635 | return nil, fmt.Errorf(
636 | "template: invalid name %q", name)
637 | }
638 |
639 | kind := TEXT
640 | if path.Ext(ns) == ".html" {
641 | kind = HTML
642 | }
643 |
644 | // 再次检查使用的模板名
645 | names := map[string]bool{}
646 |
647 | err := parseText(t, names, kind, ns, text)
648 |
649 | if err != nil {
650 | return nil, err
651 | }
652 |
653 | filename := []string{}
654 |
655 | for k, toload := range names {
656 | if toload {
657 | filename = append(filename, k)
658 | }
659 | }
660 |
661 | // 递归载入文件
662 | if len(filename) != 0 {
663 | err = parseFiles(t, filename...)
664 | }
665 |
666 | if err != nil {
667 | return nil, err
668 | }
669 |
670 | return t.Lookup(ns), nil
671 | }
672 |
673 | /**
674 | ParseGlob 解析多个模板文件.
675 | 自动跳过重复的文件.
676 | 参数:
677 | pattern 模板文件模式匹配.
678 | 返回:
679 | 是否有错误发生.
680 | */
681 | func (t *Template) ParseGlob(pattern string) error {
682 | filename, err := filepath.Glob(pattern)
683 | if err != nil {
684 | return err
685 | }
686 | fmt.Println(filename)
687 | return t.ParseFiles(filename...)
688 | }
689 |
690 | /**
691 | RootDir 返回 rootdir.
692 | */
693 | func (t *Template) RootDir() string {
694 | return t.base.RootDir()
695 | }
696 |
697 | /**
698 | Walk 遍历 dir, 根据允许的扩展名加载模板文件.
699 | 要求所有加载文件必须位于 rootdir 之下. 自动跳过重复的文件.
700 | 参数:
701 | dir 要遍历的目录.
702 | exts 允许的扩展名拼接字符串, 格式实例: ".html.tmpl".
703 | */
704 | func (t *Template) Walk(dir string, exts string) error {
705 |
706 | filename := []string{}
707 | filepath.Walk(dir,
708 | func(name string, info os.FileInfo, err error) error {
709 |
710 | if err != nil || info.IsDir() {
711 | return nil
712 | }
713 |
714 | name = clearSlash(name)
715 | ext := path.Ext(name)
716 | if ext == "" {
717 | return nil
718 | }
719 |
720 | pos := strings.Index(exts, ext) + len(ext)
721 | if pos >= len(ext) && (pos == len(exts) || exts[pos] == '.') {
722 | filename = append(filename, name)
723 | }
724 |
725 | return nil
726 | })
727 |
728 | return t.ParseFiles(filename...)
729 | }
730 |
--------------------------------------------------------------------------------
/template_test.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "bytes"
5 | "github.com/achun/testing-want"
6 | "os"
7 | "path"
8 | "path/filepath"
9 | "strings"
10 | "testing"
11 | "text/template/parse"
12 | )
13 |
14 | var pwd = initPWD()
15 |
16 | func initPWD() string {
17 | pwd, err := os.Getwd()
18 | if err != nil {
19 | os.Exit(1)
20 | }
21 | return filepath.ToSlash(pwd)
22 | }
23 |
24 | func walkTree(node parse.Node) {
25 |
26 | switch n := node.(type) {
27 | case *parse.ListNode:
28 | for _, node := range n.Nodes {
29 | walkTree(node)
30 | }
31 | return
32 | }
33 | }
34 |
35 | var uriTests = []string{
36 | ".",
37 | "b",
38 | "../a",
39 | "../a/b",
40 | "../a/b/",
41 | "../a/b/./c/../../.././a",
42 | `$`,
43 | `$/.`,
44 | `C:\a/..\a/b`,
45 | `C:/a\\..\a/b/\`,
46 | `#/a/..\a/b`,
47 | `#/a/..\a/b/\`,
48 | `\\/\#/a/..\a/b/\`,
49 | `#/a\\/b///c\..\../\.././a`,
50 | }
51 |
52 | var fixtures = map[string]interface{}{
53 | "title": `>title`,
54 | "body": `/admin/body.html`,
55 | "js": `/admin/js.tmpl`,
56 | "href": ">>>",
57 | "name": "admin",
58 | }
59 |
60 | func TestCleanURI(T *testing.T) {
61 | wt := want.T(T)
62 | wd, _ := os.Getwd()
63 | wt.Equal(clearSlash(wd), pwd)
64 | for _, s := range uriTests {
65 |
66 | n := path.Clean(strings.Replace(s, "\\", "/", -1))
67 | if n[0] == '.' {
68 | n = ""
69 | }
70 | wt.Equal(cleanURI(s), n, s)
71 | }
72 | }
73 |
74 | func TestTemplate(T *testing.T) {
75 | var buf bytes.Buffer
76 |
77 | wt := want.T(T)
78 | t, err := New("./fixtures/base/layout.html")
79 | wt.Nil(err)
80 | wt.Equal(t.RootDir(), pwd+`/fixtures/base`)
81 | wt.Equal(t.Name(), pwd+"/fixtures/base/layout.html")
82 | t.Walk(pwd+`/fixtures/base`, ".html.tmpl")
83 |
84 | wt.Nil(t.Execute(&buf, fixtures))
85 | wt.Equal(buf.String(),
86 | `
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | `)
96 | }
97 |
--------------------------------------------------------------------------------
/text.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "io"
5 | "text/template"
6 | "text/template/parse"
7 | )
8 |
9 | type FuncMap template.FuncMap
10 |
11 | type textTemp struct {
12 | *template.Template
13 | }
14 |
15 | /**
16 | newText 新建基于 "text/template" 的共享模板.
17 | */
18 | func newText() executor {
19 | return textTemp{
20 | Template: template.New(shareName),
21 | }
22 | }
23 |
24 | func (t textTemp) AddParseTree(tree *parse.Tree) (executor, error) {
25 |
26 | nt, err := t.Template.New(tree.Name).AddParseTree(tree.Name, tree)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | if t.Template.Tree == nil {
32 | t.Template = nt
33 | return t, nil
34 | }
35 |
36 | return textTemp{
37 | Template: nt,
38 | }, nil
39 | }
40 |
41 | func (t textTemp) Execute(
42 | p *Template, wr io.Writer, data interface{}) error {
43 | return t.Template.Execute(wr, data)
44 | }
45 |
46 | func (t textTemp) Funcs(funcMap FuncMap) {
47 | t.Template.Funcs(template.FuncMap(funcMap))
48 | }
49 |
50 | func (t textTemp) Lookup(name string) executor {
51 |
52 | nt := t.Template.Lookup(name)
53 | if nt == nil {
54 | return nil
55 | }
56 |
57 | return textTemp{
58 | Template: nt,
59 | }
60 | }
61 |
62 | func (t textTemp) Kind() Kind {
63 | return TEXT
64 | }
65 |
66 | func (t textTemp) Tree() *parse.Tree {
67 | return t.Template.Tree
68 | }
69 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "path"
7 | "strconv"
8 | "strings"
9 | "text/template/parse"
10 | )
11 |
12 | type treeReceiver func(tree *parse.Tree, kind Kind) error
13 |
14 | /**
15 | 共享模板的名字, 共享模板永远不被执行.
16 | 用于优化 Clone(), 共享 common, FuncMap.
17 | */
18 | const shareName = "57b11edbe4825b57ab27b7beab9848ed"
19 |
20 | /**
21 | 从文件进行读取, 解析, 命名, hack
22 | filename 是经过 clear 后的绝对路径.
23 | */
24 | func parseFiles(t *Template, filename ...string) error {
25 |
26 | var (
27 | kind Kind
28 | )
29 |
30 | // 再次检查使用的模板名
31 | names := map[string]bool{}
32 |
33 | for _, name := range filename {
34 |
35 | switch path.Ext(name) {
36 | case "":
37 | return fmt.Errorf("template: invalid file name: %q", name)
38 | case ".html":
39 | kind = HTML
40 | default:
41 | kind = TEXT
42 | }
43 |
44 | // 懒方法, 直接跳过
45 | if t.base.Lookup(name).IsValid() {
46 | continue
47 | }
48 |
49 | b, err := ioutil.ReadFile(name)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | err = parseText(t, names, kind, name, string(b))
55 | if err != nil {
56 | return err
57 | }
58 |
59 | }
60 |
61 | filename = []string{}
62 |
63 | for k, toload := range names {
64 | if toload {
65 | filename = append(filename, k)
66 | }
67 | }
68 |
69 | // 递归载入文件
70 | if len(filename) != 0 {
71 | return parseFiles(t, filename...)
72 | }
73 |
74 | return nil
75 | }
76 |
77 | /**
78 | parseText 需要知道第一个 tree 的 kind. 以便添加到 t.
79 | 可能会载入新的文件, 产生模板名称对应的模板尚未载入.
80 | 直到全部解析完才会完整载入.
81 | 参数 ns 是给 tree 的 uri 命名.
82 | */
83 | func parseText(t *Template, names map[string]bool,
84 | kind Kind, ns, text string) error {
85 |
86 | var name string
87 |
88 | trees, err := parse.Parse(ns, text,
89 | t.leftDelim, t.rightDelim, placeholderFuncs, t.base.funcs)
90 |
91 | if err != nil {
92 | return err
93 | }
94 |
95 | rootdir := t.RootDir()
96 | dir := absPath(ns)
97 |
98 | for from, tree := range trees {
99 |
100 | name = from
101 | // define 内嵌模板不能有扩展名
102 | if name != ns {
103 |
104 | if path.Ext(name) != "" {
105 | return fmt.Errorf(
106 | "template: extension are not supported on define %q", from)
107 | }
108 | name = relToURI(rootdir, dir, name)
109 | }
110 |
111 | if name == "" {
112 | return fmt.Errorf("template: is invalid on define %q", from)
113 | }
114 |
115 | // 需要再次检查模板是否被载入
116 | if t.base.Lookup(name).IsValid() {
117 | return fmt.Errorf(
118 | "template: redefinition of %q", name)
119 | }
120 |
121 | tree.Name = name
122 | tree.ParseName = ns
123 |
124 | err = hackNode(t, names, name, tree.Root, -1, tree.Root)
125 |
126 | if err == nil {
127 | _, err = t.AddParseTree(kind, tree)
128 | }
129 |
130 | if err != nil {
131 | return err
132 | }
133 | }
134 | names[ns] = false
135 | return nil
136 | }
137 |
138 | /**
139 | hackNode 对模板中 template/import 的目标模板重新命名.
140 | 规则:
141 | 没有扩展名当作内嵌模板, 反之当作文件模板.
142 | 目标名称变更为绝对路径名.
143 | 所有 template 用 import 替换. 格式为:
144 | import "from" "target" args...
145 |
146 | 参数:
147 | t 模板.
148 | names 所有使用的模板需要检查是否已经载入.
149 | from 来源模板名. 绝对路径.
150 | node 待 hack 的原始 parse.Node.
151 |
152 | 返回:
153 | 是否有错误发生的错误.
154 | */
155 | func hackNode(t *Template, names map[string]bool, from string,
156 | list *parse.ListNode, i int, node parse.Node) error {
157 |
158 | var (
159 | pipe *parse.PipeNode
160 | args []parse.Node
161 | target *parse.StringNode
162 | )
163 | rootdir := t.RootDir()
164 |
165 | switch n := node.(type) {
166 | default:
167 | return nil
168 | case *parse.ListNode:
169 | for i, node := range n.Nodes {
170 | err := hackNode(t, names, from, n, i, node)
171 | if err != nil {
172 | return err
173 | }
174 | }
175 | return nil
176 | case *parse.TemplateNode:
177 |
178 | args = make([]parse.Node, 3)
179 |
180 | args[0] = parse.NewIdentifier("import").SetPos(n.Pos)
181 |
182 | // from, 保存调用者
183 | args[1] = &parse.StringNode{
184 | NodeType: parse.NodeString,
185 | Pos: n.Position(), // 伪造
186 | Quoted: strconv.Quote(from),
187 | Text: from,
188 | }
189 |
190 | // target, 重建目标
191 | args[2] = &parse.StringNode{
192 | NodeType: parse.NodeString,
193 | Pos: n.Position(), // 伪造
194 | Quoted: strconv.Quote(n.Name),
195 | Text: n.Name,
196 | }
197 |
198 | // 复制其它参数
199 | pipe = n.Pipe
200 | if pipe != nil &&
201 | len(pipe.Cmds) != 0 &&
202 | pipe.Cmds[0].NodeType == parse.NodeCommand {
203 |
204 | for _, arg := range pipe.Cmds[0].Args {
205 | args = append(args, arg)
206 | }
207 | } else {
208 | if pipe == nil {
209 | pipe = &parse.PipeNode{
210 | NodeType: parse.NodePipe,
211 | Pos: n.Position(), // 伪造
212 | Line: n.Line,
213 | Cmds: []*parse.CommandNode{
214 | &parse.CommandNode{
215 | NodeType: parse.NodeCommand,
216 | Pos: n.Position(),
217 | },
218 | },
219 | }
220 | }
221 | }
222 |
223 | pipe.Cmds[0].Args = args
224 |
225 | // 改成 ActionNode
226 | list.Nodes[i] = &parse.ActionNode{
227 | NodeType: parse.NodeAction,
228 | Pos: n.Pos,
229 | Line: n.Line,
230 | Pipe: pipe,
231 | }
232 |
233 | case *parse.ActionNode:
234 |
235 | pipe = n.Pipe
236 |
237 | if pipe == nil ||
238 | len(pipe.Decl) != 0 ||
239 | len(pipe.Cmds) == 0 ||
240 | pipe.Cmds[0].NodeType != parse.NodeCommand ||
241 | len(pipe.Cmds[0].Args) == 0 ||
242 | pipe.Cmds[0].Args[0].Type() != parse.NodeIdentifier ||
243 | pipe.Cmds[0].Args[0].String() != "import" {
244 |
245 | return nil
246 | }
247 |
248 | args = make([]parse.Node, len(pipe.Cmds[0].Args)+1)
249 | args[0] = pipe.Cmds[0].Args[0]
250 |
251 | // from, 增加调用者来源
252 | args[1] = &parse.StringNode{
253 | NodeType: parse.NodeString,
254 | Pos: args[0].Position(), // 伪造
255 | Quoted: strconv.Quote(from),
256 | Text: from,
257 | }
258 | // 复制其它参数
259 | for i, arg := range pipe.Cmds[0].Args {
260 | if i != 0 {
261 | args[i+1] = arg
262 | }
263 | }
264 | pipe.Cmds[0].Args = args
265 | }
266 |
267 | // 处理目标模板 args[2], 有可能是变量.
268 | target, _ = args[2].(*parse.StringNode)
269 | if target == nil {
270 | return nil
271 | }
272 |
273 | // 计算目标路径
274 | name := relToURI(rootdir, absPath(from), target.Text)
275 | if name == "" {
276 | return fmt.Errorf(
277 | "template: is invalid on define %q", target.Text)
278 | }
279 |
280 | // 判断文件模板是否载入
281 | if path.Ext(name) != "" &&
282 | !t.base.Lookup(name).IsValid() {
283 |
284 | names[name] = true
285 | }
286 |
287 | target.Text = name
288 | target.Quoted = strconv.Quote(name)
289 |
290 | return nil
291 | }
292 |
293 | func absPath(name string) string {
294 |
295 | if path.Ext(name) != "" {
296 | return path.Dir(name)
297 | }
298 | return path.Dir(path.Dir(name))
299 | }
300 |
301 | /**
302 | absToURI 根据绝对 uri 路径 rootdir, 计算绝对路径 name 的 uri 路径.
303 | 如果返回值为 "" 表示 name 非法.
304 | */
305 | func absToURI(rootdir, name string) string {
306 |
307 | name = cleanURI(name)
308 | if name == "" ||
309 | len(name) <= len(rootdir) ||
310 | name[len(rootdir)] != '/' ||
311 | name[:len(rootdir)] != rootdir {
312 |
313 | return ""
314 | }
315 | return name
316 | }
317 |
318 | /**
319 | clearSlash 清理 s 中的反斜杠, 连续反斜杠, 连续斜杠.
320 | */
321 | func clearSlash(s string) string {
322 | var to []byte
323 |
324 | if len(s) == 0 {
325 | return s
326 | }
327 |
328 | b := -1
329 | for i := 0; i < len(s); i++ {
330 | c := s[i]
331 | if c == '\\' || c == '/' {
332 | if b == -1 {
333 | b = i
334 | continue
335 | }
336 | c = '/'
337 | }
338 |
339 | if b != -1 {
340 | if to == nil {
341 | // 连续斜杠
342 | if c == '/' && (s[i-1] == '\\' || s[i-1] == '/') {
343 | continue
344 | }
345 |
346 | to = make([]byte, 0, len(s))
347 | to = append(to, s[:b]...)
348 | }
349 | b = -1
350 | to = append(to, '/')
351 | }
352 |
353 | if to != nil {
354 | to = append(to, c)
355 | }
356 | }
357 |
358 | if len(to) == 0 {
359 | return s
360 | }
361 | return string(to)
362 | }
363 |
364 | /**
365 | cleanURI 返回 uri 的最短路径写法.
366 | */
367 | func cleanURI(uri string) string {
368 |
369 | uri = clearSlash(uri)
370 |
371 | if uri == "" {
372 | return ""
373 | }
374 |
375 | if strings.Index(uri, "/.") != -1 {
376 | uri = path.Clean(uri)
377 | }
378 |
379 | if uri[0] == '.' {
380 | return ""
381 | }
382 |
383 | return uri
384 | }
385 |
386 | /**
387 | relToURI 根据绝对路径 rootdir 和 dir, 计算 name 的绝对 uri 路径.
388 | 如果返回值为 "" 表示 name 非法.
389 | 参数:
390 | rootdir 根目录绝对路径
391 | dir 当前目录绝对路径, 如果 dir 为空, 以 rootdir 替代.
392 | name 已经 clean 后的相对路径.
393 | 以 "/" 开头以 rootdir 计算, 否则以 dir 计算.
394 | */
395 | func relToURI(rootdir, dir, name string) string {
396 |
397 | if name == "" {
398 | return ""
399 | }
400 |
401 | if name[0] == '/' {
402 | return absToURI(rootdir, rootdir+name)
403 | }
404 |
405 | if dir == "" {
406 | return absToURI(rootdir, rootdir+"/"+name)
407 | }
408 |
409 | return absToURI(rootdir, dir+"/"+name)
410 | }
411 |
--------------------------------------------------------------------------------