├── .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

{{.name}}

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 |

admin

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 |

{{.name}}

-------------------------------------------------------------------------------- /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 |

admin

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 | --------------------------------------------------------------------------------