404 lignes
11 Kio
Go
404 lignes
11 Kio
Go
|
// Copyright 2016 José Santos <henrique_1609@me.com>
|
||
|
//
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
// Jet is a fast and dynamic template engine for the Go programming language, set of features
|
||
|
// includes very fast template execution, a dynamic and flexible language, template inheritance, low number of allocations,
|
||
|
// special interfaces to allow even further optimizations.
|
||
|
|
||
|
package jet
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"path"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"text/template"
|
||
|
)
|
||
|
|
||
|
// Set is responsible to load,invoke parse and cache templates and relations
|
||
|
// every jet template is associated with one set.
|
||
|
// create a set with jet.NewSet(escapeeFn) returns a pointer to the Set
|
||
|
type Set struct {
|
||
|
loader Loader
|
||
|
templates map[string]*Template // parsed templates
|
||
|
escapee SafeWriter // escapee to use at runtime
|
||
|
globals VarMap // global scope for this template set
|
||
|
tmx *sync.RWMutex // template parsing mutex
|
||
|
gmx *sync.RWMutex // global variables map mutex
|
||
|
defaultExtensions []string
|
||
|
developmentMode bool
|
||
|
}
|
||
|
|
||
|
// SetDevelopmentMode set's development mode on/off, in development mode template will be recompiled on every run
|
||
|
func (s *Set) SetDevelopmentMode(b bool) *Set {
|
||
|
s.developmentMode = b
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
func (a *Set) LookupGlobal(key string) (val interface{}, found bool) {
|
||
|
a.gmx.RLock()
|
||
|
val, found = a.globals[key]
|
||
|
a.gmx.RUnlock()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// AddGlobal add or set a global variable into the Set
|
||
|
func (s *Set) AddGlobal(key string, i interface{}) *Set {
|
||
|
s.gmx.Lock()
|
||
|
if s.globals == nil {
|
||
|
s.globals = make(VarMap)
|
||
|
}
|
||
|
s.globals[key] = reflect.ValueOf(i)
|
||
|
s.gmx.Unlock()
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
func (s *Set) AddGlobalFunc(key string, fn Func) *Set {
|
||
|
return s.AddGlobal(key, fn)
|
||
|
}
|
||
|
|
||
|
// NewSetLoader creates a new set with custom Loader
|
||
|
func NewSetLoader(escapee SafeWriter, loader Loader) *Set {
|
||
|
return &Set{loader: loader, tmx: &sync.RWMutex{}, gmx: &sync.RWMutex{}, escapee: escapee, templates: make(map[string]*Template), defaultExtensions: append([]string{}, defaultExtensions...)}
|
||
|
}
|
||
|
|
||
|
// NewHTMLSetLoader creates a new set with custom Loader
|
||
|
func NewHTMLSetLoader(loader Loader) *Set {
|
||
|
return NewSetLoader(template.HTMLEscape, loader)
|
||
|
}
|
||
|
|
||
|
// NewSet creates a new set, dirs is a list of directories to be searched for templates
|
||
|
func NewSet(escapee SafeWriter, dirs ...string) *Set {
|
||
|
return NewSetLoader(escapee, &OSFileSystemLoader{dirs: dirs})
|
||
|
}
|
||
|
|
||
|
// NewHTMLSet creates a new set, dirs is a list of directories to be searched for templates
|
||
|
func NewHTMLSet(dirs ...string) *Set {
|
||
|
return NewSet(template.HTMLEscape, dirs...)
|
||
|
}
|
||
|
|
||
|
// AddPath add path to the lookup list, when loading a template the Set will
|
||
|
// look into the lookup list for the file matching the provided name.
|
||
|
func (s *Set) AddPath(path string) {
|
||
|
if loader, ok := s.loader.(hasAddPath); ok {
|
||
|
loader.AddPath(path)
|
||
|
} else {
|
||
|
panic(fmt.Sprintf("AddPath() not supported on custom loader of type %T", s.loader))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// AddGopathPath add path based on GOPATH env to the lookup list, when loading a template the Set will
|
||
|
// look into the lookup list for the file matching the provided name.
|
||
|
func (s *Set) AddGopathPath(path string) {
|
||
|
if loader, ok := s.loader.(hasAddGopathPath); ok {
|
||
|
loader.AddGopathPath(path)
|
||
|
} else {
|
||
|
panic(fmt.Sprintf("AddGopathPath() not supported on custom loader of type %T", s.loader))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// resolveName try to resolve a template name, the steps as follow
|
||
|
// 1. try provided path
|
||
|
// 2. try provided path+defaultExtensions
|
||
|
// ex: set.resolveName("catalog/products.list") with defaultExtensions set to []string{".html.jet",".jet"}
|
||
|
// try catalog/products.list
|
||
|
// try catalog/products.list.html.jet
|
||
|
// try catalog/products.list.jet
|
||
|
func (s *Set) resolveName(name string) (newName, fileName string, foundLoaded, foundFile bool) {
|
||
|
newName = name
|
||
|
if _, foundLoaded = s.templates[newName]; foundLoaded {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if fileName, foundFile = s.loader.Exists(name); foundFile {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
for _, extension := range s.defaultExtensions {
|
||
|
newName = name + extension
|
||
|
if _, foundLoaded = s.templates[newName]; foundLoaded {
|
||
|
return
|
||
|
}
|
||
|
if fileName, foundFile = s.loader.Exists(newName); foundFile {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (s *Set) resolveNameSibling(name, sibling string) (newName, fileName string, foundLoaded, foundFile, isRelativeName bool) {
|
||
|
if sibling != "" {
|
||
|
i := strings.LastIndex(sibling, "/")
|
||
|
if i != -1 {
|
||
|
if newName, fileName, foundLoaded, foundFile = s.resolveName(path.Join(sibling[:i+1], name)); foundFile || foundLoaded {
|
||
|
isRelativeName = true
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
newName, fileName, foundLoaded, foundFile = s.resolveName(name)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Parse parses the template, this method will link the template to the set but not the set to
|
||
|
func (s *Set) Parse(name, content string) (*Template, error) {
|
||
|
sc := *s
|
||
|
sc.developmentMode = true
|
||
|
|
||
|
sc.tmx.RLock()
|
||
|
t, err := sc.parse(name, content)
|
||
|
sc.tmx.RUnlock()
|
||
|
|
||
|
return t, err
|
||
|
}
|
||
|
|
||
|
func (s *Set) loadFromFile(name, fileName string) (template *Template, err error) {
|
||
|
f, err := s.loader.Open(fileName)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
content, err := ioutil.ReadAll(f)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return s.parse(name, string(content))
|
||
|
}
|
||
|
|
||
|
func (s *Set) getTemplateWhileParsing(parentName, name string) (template *Template, err error) {
|
||
|
name = path.Clean(name)
|
||
|
|
||
|
if s.developmentMode {
|
||
|
if newName, fileName, _, foundPath, _ := s.resolveNameSibling(name, parentName); foundPath {
|
||
|
return s.loadFromFile(newName, fileName)
|
||
|
} else {
|
||
|
return nil, fmt.Errorf("template %s can't be loaded", name)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if newName, fileName, foundLoaded, foundPath, isRelative := s.resolveNameSibling(name, parentName); foundPath {
|
||
|
template, err = s.loadFromFile(newName, fileName)
|
||
|
s.templates[newName] = template
|
||
|
|
||
|
if !isRelative {
|
||
|
s.templates[name] = template
|
||
|
}
|
||
|
} else if foundLoaded {
|
||
|
template = s.templates[newName]
|
||
|
if !isRelative && name != newName {
|
||
|
s.templates[name] = template
|
||
|
}
|
||
|
} else {
|
||
|
err = fmt.Errorf("template %s can't be loaded", name)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// getTemplate gets a template already loaded by name
|
||
|
func (s *Set) getTemplate(name, sibling string) (template *Template, err error) {
|
||
|
name = path.Clean(name)
|
||
|
|
||
|
if s.developmentMode {
|
||
|
s.tmx.RLock()
|
||
|
defer s.tmx.RUnlock()
|
||
|
if newName, fileName, foundLoaded, foundFile, _ := s.resolveNameSibling(name, sibling); foundFile || foundLoaded {
|
||
|
if foundFile {
|
||
|
template, err = s.loadFromFile(newName, fileName)
|
||
|
} else {
|
||
|
template, _ = s.templates[newName]
|
||
|
}
|
||
|
} else {
|
||
|
err = fmt.Errorf("template %s can't be loaded", name)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
//fast path
|
||
|
s.tmx.RLock()
|
||
|
newName, fileName, foundLoaded, foundFile, isRelative := s.resolveNameSibling(name, sibling)
|
||
|
|
||
|
if foundLoaded {
|
||
|
template = s.templates[newName]
|
||
|
s.tmx.RUnlock()
|
||
|
if !isRelative && name != newName {
|
||
|
// creates an alias
|
||
|
s.tmx.Lock()
|
||
|
if _, found := s.templates[name]; !found {
|
||
|
s.templates[name] = template
|
||
|
}
|
||
|
s.tmx.Unlock()
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
s.tmx.RUnlock()
|
||
|
|
||
|
//not found parses and cache
|
||
|
s.tmx.Lock()
|
||
|
defer s.tmx.Unlock()
|
||
|
|
||
|
newName, fileName, foundLoaded, foundFile, isRelative = s.resolveNameSibling(name, sibling)
|
||
|
if foundLoaded {
|
||
|
template = s.templates[newName]
|
||
|
if !isRelative && name != newName {
|
||
|
// creates an alias
|
||
|
if _, found := s.templates[name]; !found {
|
||
|
s.templates[name] = template
|
||
|
}
|
||
|
}
|
||
|
} else if foundFile {
|
||
|
template, err = s.loadFromFile(newName, fileName)
|
||
|
|
||
|
if !isRelative && name != newName {
|
||
|
// creates an alias
|
||
|
if _, found := s.templates[name]; !found {
|
||
|
s.templates[name] = template
|
||
|
}
|
||
|
}
|
||
|
|
||
|
s.templates[newName] = template
|
||
|
} else {
|
||
|
err = fmt.Errorf("template %s can't be loaded", name)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (s *Set) GetTemplate(name string) (template *Template, err error) {
|
||
|
template, err = s.getTemplate(name, "")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (s *Set) LoadTemplate(name, content string) (template *Template, err error) {
|
||
|
if s.developmentMode {
|
||
|
s.tmx.RLock()
|
||
|
defer s.tmx.RUnlock()
|
||
|
template, err = s.parse(name, content)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
//fast path
|
||
|
var found bool
|
||
|
s.tmx.RLock()
|
||
|
if template, found = s.templates[name]; found {
|
||
|
s.tmx.RUnlock()
|
||
|
return
|
||
|
}
|
||
|
s.tmx.RUnlock()
|
||
|
|
||
|
//not found parses and cache
|
||
|
s.tmx.Lock()
|
||
|
defer s.tmx.Unlock()
|
||
|
|
||
|
if template, found = s.templates[name]; found {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if template, err = s.parse(name, content); err == nil {
|
||
|
s.templates[name] = template
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (t *Template) String() (template string) {
|
||
|
if t.extends != nil {
|
||
|
if len(t.Root.Nodes) > 0 && len(t.imports) == 0 {
|
||
|
template += fmt.Sprintf("{{extends %q}}", t.extends.ParseName)
|
||
|
} else {
|
||
|
template += fmt.Sprintf("{{extends %q}}", t.extends.ParseName)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for k, _import := range t.imports {
|
||
|
if t.extends == nil && k == 0 {
|
||
|
template += fmt.Sprintf("{{import %q}}", _import.ParseName)
|
||
|
} else {
|
||
|
template += fmt.Sprintf("\n{{import %q}}", _import.ParseName)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if t.extends != nil || len(t.imports) > 0 {
|
||
|
if len(t.Root.Nodes) > 0 {
|
||
|
template += "\n" + t.Root.String()
|
||
|
}
|
||
|
} else {
|
||
|
template += t.Root.String()
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (t *Template) addBlocks(blocks map[string]*BlockNode) {
|
||
|
if len(blocks) > 0 {
|
||
|
if t.processedBlocks == nil {
|
||
|
t.processedBlocks = make(map[string]*BlockNode)
|
||
|
}
|
||
|
for key, value := range blocks {
|
||
|
t.processedBlocks[key] = value
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type VarMap map[string]reflect.Value
|
||
|
|
||
|
func (scope VarMap) Set(name string, v interface{}) VarMap {
|
||
|
scope[name] = reflect.ValueOf(v)
|
||
|
return scope
|
||
|
}
|
||
|
|
||
|
func (scope VarMap) SetFunc(name string, v Func) VarMap {
|
||
|
scope[name] = reflect.ValueOf(v)
|
||
|
return scope
|
||
|
}
|
||
|
|
||
|
func (scope VarMap) SetWriter(name string, v SafeWriter) VarMap {
|
||
|
scope[name] = reflect.ValueOf(v)
|
||
|
return scope
|
||
|
}
|
||
|
|
||
|
// Execute executes the template in the w Writer
|
||
|
func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) error {
|
||
|
return t.ExecuteI18N(nil, w, variables, data)
|
||
|
}
|
||
|
|
||
|
type Translator interface {
|
||
|
Msg(key, defaultValue string) string
|
||
|
Trans(format, defaultFormat string, v ...interface{}) string
|
||
|
}
|
||
|
|
||
|
func (t *Template) ExecuteI18N(translator Translator, w io.Writer, variables VarMap, data interface{}) (err error) {
|
||
|
st := pool_State.Get().(*Runtime)
|
||
|
defer st.recover(&err)
|
||
|
|
||
|
st.blocks = t.processedBlocks
|
||
|
st.translator = translator
|
||
|
st.variables = variables
|
||
|
st.set = t.set
|
||
|
st.Writer = w
|
||
|
|
||
|
// resolve extended template
|
||
|
for t.extends != nil {
|
||
|
t = t.extends
|
||
|
}
|
||
|
|
||
|
if data != nil {
|
||
|
st.context = reflect.ValueOf(data)
|
||
|
}
|
||
|
|
||
|
st.executeList(t.Root)
|
||
|
return
|
||
|
}
|