github.com/naoina/kocha@v0.7.1-0.20171129072645-78c7a531f799/template.go (about)

     1  package kocha
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"html/template"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"reflect"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/naoina/kocha/util"
    15  )
    16  
    17  const (
    18  	LayoutDir        = "layout"
    19  	ErrorTemplateDir = "error"
    20  
    21  	layoutPath = LayoutDir + string(filepath.Separator)
    22  )
    23  
    24  // TemplatePathInfo represents an information of template paths.
    25  type TemplatePathInfo struct {
    26  	Name  string   // name of the application.
    27  	Paths []string // directory paths of the template files.
    28  }
    29  
    30  type templateKey struct {
    31  	appName  string
    32  	name     string
    33  	format   string
    34  	isLayout bool
    35  }
    36  
    37  func (k templateKey) String() string {
    38  	p := k.name
    39  	if k.isLayout {
    40  		p = filepath.Join(LayoutDir, p)
    41  	}
    42  	return fmt.Sprintf("%s:%s.%s", k.appName, p, k.format)
    43  }
    44  
    45  // Template represents the templates information.
    46  type Template struct {
    47  	PathInfo   TemplatePathInfo // information of location of template paths.
    48  	FuncMap    TemplateFuncMap  // same as template.FuncMap.
    49  	LeftDelim  string           // left action delimiter.
    50  	RightDelim string           // right action delimiter.
    51  
    52  	m   map[templateKey]*template.Template
    53  	app *Application
    54  }
    55  
    56  // Get gets a parsed template.
    57  func (t *Template) Get(appName, layout, name, format string) (*template.Template, error) {
    58  	key := templateKey{
    59  		appName:  appName,
    60  		format:   format,
    61  		isLayout: layout != "",
    62  	}
    63  	if key.isLayout {
    64  		key.name = layout
    65  	} else {
    66  		key.name = name
    67  	}
    68  	tmpl, exists := t.m[key]
    69  	if !exists {
    70  		return nil, fmt.Errorf("kocha: template not found: %s", key)
    71  	}
    72  	return tmpl, nil
    73  }
    74  
    75  func (t *Template) build(app *Application) (*Template, error) {
    76  	if t == nil {
    77  		t = &Template{}
    78  	}
    79  	t.app = app
    80  	if t.LeftDelim == "" {
    81  		t.LeftDelim = "{{"
    82  	}
    83  	if t.RightDelim == "" {
    84  		t.RightDelim = "}}"
    85  	}
    86  	t, err := t.buildFuncMap()
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	t, err = t.buildTemplateMap()
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	return t, nil
    95  }
    96  
    97  func (t *Template) buildFuncMap() (*Template, error) {
    98  	m := TemplateFuncMap{
    99  		"yield":           t.yield,
   100  		"in":              t.in,
   101  		"url":             t.url,
   102  		"nl2br":           t.nl2br,
   103  		"raw":             t.raw,
   104  		"invoke_template": t.invokeTemplate,
   105  		"flash":           t.flash,
   106  		"join":            t.join,
   107  	}
   108  	for name, fn := range t.FuncMap {
   109  		m[name] = fn
   110  	}
   111  	t.FuncMap = m
   112  	return t, nil
   113  }
   114  
   115  // buildTemplateMap returns templateMap constructed from templateSet.
   116  func (t *Template) buildTemplateMap() (*Template, error) {
   117  	info := t.PathInfo
   118  	var templatePaths map[string]map[string]map[string]string
   119  	if data := t.app.ResourceSet.Get("_kocha_template_paths"); data != nil {
   120  		if paths, ok := data.(map[string]map[string]map[string]string); ok {
   121  			templatePaths = paths
   122  		}
   123  	}
   124  	if templatePaths == nil {
   125  		templatePaths = map[string]map[string]map[string]string{
   126  			info.Name: make(map[string]map[string]string),
   127  		}
   128  		for _, rootPath := range info.Paths {
   129  			if err := t.collectTemplatePaths(templatePaths[info.Name], rootPath); err != nil {
   130  				return nil, err
   131  			}
   132  		}
   133  		t.app.ResourceSet.Add("_kocha_template_paths", templatePaths)
   134  	}
   135  	t.m = map[templateKey]*template.Template{}
   136  	l := len(t.LeftDelim) + len("$ := .Data") + len(t.RightDelim)
   137  	buf := bytes.NewBuffer(append(append(append(make([]byte, 0, l), t.LeftDelim...), "$ := .Data"...), t.RightDelim...))
   138  	for appName, templates := range templatePaths {
   139  		if err := t.buildAppTemplateSet(buf, l, t.m, appName, templates); err != nil {
   140  			return nil, err
   141  		}
   142  	}
   143  	return t, nil
   144  }
   145  
   146  // TemplateFuncMap is an alias of templete.FuncMap.
   147  type TemplateFuncMap template.FuncMap
   148  
   149  func (t *Template) collectTemplatePaths(templatePaths map[string]map[string]string, templateDir string) error {
   150  	return filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error {
   151  		if err != nil {
   152  			return err
   153  		}
   154  		if info.IsDir() {
   155  			return nil
   156  		}
   157  		baseName, err := filepath.Rel(templateDir, path)
   158  		if err != nil {
   159  			return err
   160  		}
   161  		name := strings.TrimSuffix(baseName, util.TemplateSuffix)
   162  		ext := filepath.Ext(name)
   163  		if _, exists := templatePaths[ext]; !exists {
   164  			templatePaths[ext] = make(map[string]string)
   165  		}
   166  		templatePaths[ext][name] = path
   167  		return nil
   168  	})
   169  }
   170  
   171  func (t *Template) buildAppTemplateSet(buf *bytes.Buffer, l int, m map[templateKey]*template.Template, appName string, templates map[string]map[string]string) error {
   172  	for ext, templateInfos := range templates {
   173  		tmpl := template.New("")
   174  		for name, path := range templateInfos {
   175  			buf.Truncate(l)
   176  			var body string
   177  			if data := t.app.ResourceSet.Get(path); data != nil {
   178  				if b, ok := data.(string); ok {
   179  					buf.WriteString(b)
   180  					body = buf.String()
   181  				}
   182  			} else {
   183  				f, err := os.Open(path)
   184  				if err != nil {
   185  					return err
   186  				}
   187  				_, err = io.Copy(buf, f)
   188  				f.Close()
   189  				if err != nil {
   190  					return err
   191  				}
   192  				body = buf.String()
   193  				t.app.ResourceSet.Add(path, body)
   194  			}
   195  			if _, err := tmpl.New(name).Delims(t.LeftDelim, t.RightDelim).Funcs(template.FuncMap(t.FuncMap)).Parse(body); err != nil {
   196  				return err
   197  			}
   198  		}
   199  		for _, t := range tmpl.Templates() {
   200  			key := templateKey{
   201  				appName: appName,
   202  				name:    strings.TrimSuffix(t.Name(), ext),
   203  				format:  ext[1:], // truncate the leading dot.
   204  			}
   205  			if strings.HasPrefix(key.name, layoutPath) {
   206  				key.isLayout = true
   207  				key.name = key.name[len(layoutPath):]
   208  			}
   209  			m[key] = t
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  func (t *Template) yield(c *Context) (template.HTML, error) {
   216  	tmpl, err := t.Get(t.app.Config.AppName, "", c.Name, c.Format)
   217  	if err != nil {
   218  		return "", err
   219  	}
   220  	buf := bufPool.Get().(*bytes.Buffer)
   221  	defer func() {
   222  		buf.Reset()
   223  		bufPool.Put(buf)
   224  	}()
   225  	if err := tmpl.Execute(buf, c); err != nil {
   226  		return "", err
   227  	}
   228  	return template.HTML(buf.String()), nil
   229  }
   230  
   231  // in is for "in" template function.
   232  func (t *Template) in(a, b interface{}) (bool, error) {
   233  	v := reflect.ValueOf(a)
   234  	switch v.Kind() {
   235  	case reflect.Slice, reflect.Array, reflect.String:
   236  		if v.IsNil() {
   237  			return false, nil
   238  		}
   239  		for i := 0; i < v.Len(); i++ {
   240  			if v.Index(i).Interface() == b {
   241  				return true, nil
   242  			}
   243  		}
   244  	default:
   245  		return false, fmt.Errorf("valid types are slice, array and string, got `%s'", v.Kind())
   246  	}
   247  	return false, nil
   248  }
   249  
   250  // url is for "url" template function.
   251  func (t *Template) url(name string, v ...interface{}) (string, error) {
   252  	return t.app.Router.Reverse(name, v...)
   253  }
   254  
   255  // nl2br is for "nl2br" template function.
   256  func (t *Template) nl2br(text string) template.HTML {
   257  	return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
   258  }
   259  
   260  // raw is for "raw" template function.
   261  func (t *Template) raw(text string) template.HTML {
   262  	return template.HTML(text)
   263  }
   264  
   265  // invokeTemplate is for "invoke_template" template function.
   266  func (t *Template) invokeTemplate(unit Unit, tmplName, defTmplName string, ctx ...*Context) (html template.HTML, err error) {
   267  	var c *Context
   268  	switch len(ctx) {
   269  	case 0: // do nothing.
   270  	case 1:
   271  		c = ctx[0]
   272  	default:
   273  		return "", fmt.Errorf("number of context must be 0 or 1")
   274  	}
   275  	t.app.Invoke(unit, func() {
   276  		if html, err = t.readPartialTemplate(tmplName, c); err != nil {
   277  			// TODO: logging error.
   278  			panic(ErrInvokeDefault)
   279  		}
   280  	}, func() {
   281  		html, err = t.readPartialTemplate(defTmplName, c)
   282  	})
   283  	return html, err
   284  }
   285  
   286  // flash is for "flash" template function.
   287  // This is a shorthand for {{.Flash.Get "success"}} in template.
   288  func (t *Template) flash(c *Context, key string) string {
   289  	return c.Flash.Get(key)
   290  }
   291  
   292  // join is for "join" template function.
   293  func (t *Template) join(a interface{}, sep string) (string, error) {
   294  	v := reflect.ValueOf(a)
   295  	switch v.Kind() {
   296  	case reflect.Slice, reflect.Array:
   297  		// do nothing.
   298  	default:
   299  		return "", fmt.Errorf("valid types of first argument are slice or array, got `%s'", v.Kind())
   300  	}
   301  	if v.Len() == 0 {
   302  		return "", nil
   303  	}
   304  	buf := append(make([]byte, 0, v.Len()*2-1), fmt.Sprint(v.Index(0).Interface())...)
   305  	for i := 1; i < v.Len(); i++ {
   306  		buf = append(append(buf, sep...), fmt.Sprint(v.Index(i).Interface())...)
   307  	}
   308  	return string(buf), nil
   309  }
   310  
   311  func (t *Template) readPartialTemplate(name string, c *Context) (template.HTML, error) {
   312  	tmpl, err := t.Get(t.app.Config.AppName, "", name, "html")
   313  	if err != nil {
   314  		return "", err
   315  	}
   316  	buf := bufPool.Get().(*bytes.Buffer)
   317  	defer func() {
   318  		buf.Reset()
   319  		bufPool.Put(buf)
   320  	}()
   321  	if err := tmpl.Execute(buf, c); err != nil {
   322  		return "", err
   323  	}
   324  	return template.HTML(buf.String()), nil
   325  }
   326  
   327  func errorTemplateName(code int) string {
   328  	return filepath.Join(ErrorTemplateDir, strconv.Itoa(code))
   329  }