go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/templates/loaders.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package templates 16 17 import ( 18 "context" 19 "errors" 20 "html/template" 21 "io" 22 "io/fs" 23 "path/filepath" 24 "strings" 25 ) 26 27 // AssetsLoader returns Loader that loads templates from the given assets map. 28 // 29 // The directory with templates is expected to have special structure: 30 // - 'pages/' contain all top-level templates that will be loaded 31 // - 'includes/' contain templates that will be associated with every top-level template from 'pages/'. 32 // - 'widgets/' contain all standalone templates that will be loaded without templates from 'includes/' 33 // 34 // Only templates from 'pages/' and 'widgets/' are included in the output map. 35 func AssetsLoader(assets map[string]string) Loader { 36 return func(c context.Context, funcMap template.FuncMap) (map[string]*template.Template, error) { 37 // Pick all includes. 38 includes := []string(nil) 39 for name, body := range assets { 40 if strings.HasPrefix(name, "includes/") { 41 includes = append(includes, body) 42 } 43 } 44 45 // Parse all top level templates and associate them with all includes. 46 toplevel := map[string]*template.Template{} 47 for name, body := range assets { 48 if strings.HasPrefix(name, "pages/") { 49 t := template.New(name).Funcs(funcMap) 50 // TODO(vadimsh): There's probably a way to avoid reparsing includes 51 // all the time. 52 for _, includeSrc := range includes { 53 if _, err := t.Parse(includeSrc); err != nil { 54 return nil, err 55 } 56 } 57 58 if _, err := t.Parse(body); err != nil { 59 return nil, err 60 } 61 toplevel[name] = t 62 } 63 } 64 65 for name, body := range assets { 66 if strings.HasPrefix(name, "widgets/") { 67 t := template.New(name).Funcs(funcMap) 68 if _, err := t.Parse(body); err != nil { 69 return nil, err 70 } 71 toplevel[name] = t 72 } 73 } 74 75 return toplevel, nil 76 } 77 } 78 79 // FileSystemLoader returns Loader that loads templates from file system. 80 // 81 // The directory with templates is expected to have special structure: 82 // - 'pages/' contain all top-level templates that will be loaded 83 // - 'includes/' contain templates that will be associated with every top-level template from 'pages/'. 84 // - 'widgets/' contain all standalone templates that will be loaded without templates from 'includes/' 85 // 86 // Only templates from 'pages/' and 'widgets/' are included in the output map. 87 func FileSystemLoader(fs fs.FS) Loader { 88 return func(c context.Context, funcMap template.FuncMap) (map[string]*template.Template, error) { 89 // Read all relevant files into memory. It's ok, they are small. 90 files := map[string]string{} 91 for _, sub := range []string{"includes", "pages", "widgets"} { 92 if err := readFilesInDir(fs, sub, files); err != nil { 93 return nil, err 94 } 95 } 96 return AssetsLoader(files)(c, funcMap) 97 } 98 } 99 100 // readFilesInDir loads content of files into a map "file path" => content. 101 // 102 // Used only for small HTML templates, and thus it's fine to load them 103 // in memory. 104 // 105 // Does nothing is 'dir' is missing. 106 func readFilesInDir(fileSystem fs.FS, dir string, out map[string]string) error { 107 if _, err := fs.Stat(fileSystem, dir); errors.Is(err, fs.ErrNotExist) { 108 return nil 109 } 110 return fs.WalkDir(fileSystem, dir, func(path string, d fs.DirEntry, err error) error { 111 switch { 112 case err != nil: 113 return err 114 case d.IsDir(): 115 return nil 116 default: 117 file, err := fileSystem.Open(path) 118 if err != nil { 119 return err 120 } 121 defer func() { _ = file.Close() }() 122 contents, err := io.ReadAll(file) 123 if err != nil { 124 return err 125 } 126 out[filepath.ToSlash(path)] = string(contents) 127 return nil 128 } 129 }) 130 }