mirrored from https://chromium.googlesource.com/infra/luci/luci-go
/
loaders.go
130 lines (121 loc) · 4.04 KB
/
loaders.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// 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"
"errors"
"html/template"
"io"
"io/fs"
"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(fs fs.FS) Loader {
return func(c context.Context, funcMap template.FuncMap) (map[string]*template.Template, error) {
// Read all relevant files into memory. It's ok, they are small.
files := map[string]string{}
for _, sub := range []string{"includes", "pages", "widgets"} {
if err := readFilesInDir(fs, sub, files); err != nil {
return nil, err
}
}
return AssetsLoader(files)(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(fileSystem fs.FS, dir string, out map[string]string) error {
if _, err := fs.Stat(fileSystem, dir); errors.Is(err, fs.ErrNotExist) {
return nil
}
return fs.WalkDir(fileSystem, dir, func(path string, d fs.DirEntry, err error) error {
switch {
case err != nil:
return err
case d.IsDir():
return nil
default:
file, err := fileSystem.Open(path)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
contents, err := io.ReadAll(file)
if err != nil {
return err
}
out[filepath.ToSlash(path)] = string(contents)
return nil
}
})
}