blob: 4de7429b289f5b337b7f1b56294b0f5a356f7a2a [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// 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.
package templates
import (
"context"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// AssetsLoader returns Loader that loads templates from the given assets map.
//
// The directory with templates is expected to have special structure:
// * 'pages/' contain all top-level templates that will be loaded
// * 'includes/' contain templates that will be associated with every top-level template from 'pages/'.
// * 'widgets/' contain all standalone templates that will be loaded without templates from 'includes/'
// Only templates from 'pages/' and 'widgets/' are included in the output map.
func AssetsLoader(assets map[string]string) Loader {
return func(c context.Context, funcMap template.FuncMap) (map[string]*template.Template, error) {
// Pick all includes.
includes := []string(nil)
for name, body := range assets {
if strings.HasPrefix(name, "includes/") {
includes = append(includes, body)
}
}
// Parse all top level templates and associate them with all includes.
toplevel := map[string]*template.Template{}
for name, body := range assets {
if strings.HasPrefix(name, "pages/") {
t := template.New(name).Funcs(funcMap)
// TODO(vadimsh): There's probably a way to avoid reparsing includes
// all the time.
for _, includeSrc := range includes {
if _, err := t.Parse(includeSrc); err != nil {
return nil, err
}
}
if _, err := t.Parse(body); err != nil {
return nil, err
}
toplevel[name] = t
}
}
for name, body := range assets {
if strings.HasPrefix(name, "widgets/") {
t := template.New(name).Funcs(funcMap)
if _, err := t.Parse(body); err != nil {
return nil, err
}
toplevel[name] = t
}
}
return toplevel, nil
}
}
// FileSystemLoader returns Loader that loads templates from file system.
//
// The directory with templates is expected to have special structure:
// * 'pages/' contain all top-level templates that will be loaded
// * 'includes/' contain templates that will be associated with every top-level template from 'pages/'.
// * 'widgets/' contain all standalone templates that will be loaded without templates from 'includes/'
// Only templates from 'pages/' and 'widgets/' are included in the output map.
func FileSystemLoader(path string) Loader {
return func(c context.Context, funcMap template.FuncMap) (map[string]*template.Template, error) {
abs, err := filepath.Abs(path)
if err != nil {
return nil, err
}
// Read all relevant files into memory. It's ok, they are small.
files := map[string]string{}
if err = readFilesInDir(filepath.Join(abs, "includes"), files); err != nil {
return nil, err
}
if err = readFilesInDir(filepath.Join(abs, "pages"), files); err != nil {
return nil, err
}
if err = readFilesInDir(filepath.Join(abs, "widgets"), files); err != nil {
return nil, err
}
// Convert to assets map as consumed by AssetsLoader (relative slash
// separated paths) and reuse AssetsLoader.
assets := map[string]string{}
for k, v := range files {
rel, err := filepath.Rel(abs, k)
if err != nil {
return nil, err
}
assets[filepath.ToSlash(rel)] = v
}
return AssetsLoader(assets)(c, funcMap)
}
}
// readFilesInDir loads content of files into a map "file path" => content.
//
// Used only for small HTML templates, and thus it's fine to load them
// in memory.
//
// Does nothing is 'dir' is missing.
func readFilesInDir(dir string, out map[string]string) error {
_, err := os.Stat(dir)
if os.IsNotExist(err) {
return nil
}
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
body, err := ioutil.ReadFile(path)
if err == nil {
out[path] = string(body)
}
return err
})
}