github.com/segakazzz/buffalo@v0.16.22-0.20210119082501-1f52048d3feb/render/template.go (about) 1 package render 2 3 import ( 4 "fmt" 5 "html/template" 6 "io" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 12 "github.com/gobuffalo/packd" 13 "github.com/segakazzz/buffalo/internal/takeon/github.com/gobuffalo/syncx" 14 "github.com/segakazzz/buffalo/internal/takeon/github.com/markbates/errx" 15 "github.com/sirupsen/logrus" 16 ) 17 18 type templateRenderer struct { 19 *Engine 20 contentType string 21 names []string 22 aliases syncx.StringMap 23 } 24 25 func (s templateRenderer) ContentType() string { 26 return s.contentType 27 } 28 29 func (s templateRenderer) resolve(name string) ([]byte, error) { 30 if s.TemplatesBox == nil { 31 return nil, fmt.Errorf("no templates box is defined") 32 } 33 34 if s.TemplatesBox.Has(name) { 35 return s.TemplatesBox.Find(name) 36 } 37 38 v, ok := s.aliases.Load(name) 39 if !ok { 40 return nil, fmt.Errorf("could not find template %s", name) 41 } 42 43 return s.TemplatesBox.Find(v) 44 } 45 46 func (s *templateRenderer) Render(w io.Writer, data Data) error { 47 if s.TemplatesBox != nil { 48 err := s.TemplatesBox.Walk(func(p string, f packd.File) error { 49 base := filepath.Base(p) 50 51 dir := filepath.Dir(p) 52 53 var exts []string 54 sep := strings.Split(base, ".") 55 if len(sep) >= 1 { 56 base = sep[0] 57 } 58 if len(sep) > 1 { 59 exts = sep[1:] 60 } 61 62 for _, ext := range exts { 63 pn := filepath.Join(dir, base+"."+ext) 64 s.aliases.Store(pn, p) 65 } 66 67 return nil 68 }) 69 if err != nil { 70 return err 71 } 72 } 73 74 var body template.HTML 75 var err error 76 for _, name := range s.names { 77 body, err = s.exec(name, data) 78 if err != nil { 79 return errx.Wrap(err, name) 80 } 81 data["yield"] = body 82 } 83 w.Write([]byte(body)) 84 return nil 85 } 86 87 func fixExtension(name string, ct string) string { 88 if filepath.Ext(name) == "" { 89 switch { 90 case strings.Contains(ct, "html"): 91 name += ".html" 92 case strings.Contains(ct, "javascript"): 93 name += ".js" 94 case strings.Contains(ct, "markdown"): 95 name += ".md" 96 } 97 } 98 return name 99 } 100 101 // partialFeeder returns template string for the name from `TemplateBox`. 102 // It should be registered as helper named `partialFeeder` so plush can 103 // find it with the name. 104 func (s templateRenderer) partialFeeder(name string) (string, error) { 105 ct := strings.ToLower(s.contentType) 106 107 d, f := filepath.Split(name) 108 name = filepath.Join(d, "_"+f) 109 name = fixExtension(name, ct) 110 111 b, err := s.resolve(name) 112 return string(b), err 113 } 114 115 func (s templateRenderer) exec(name string, data Data) (template.HTML, error) { 116 ct := strings.ToLower(s.contentType) 117 data["contentType"] = ct 118 119 name = fixExtension(name, ct) 120 121 // Try to use localized version 122 templateName := s.localizedName(name, data) 123 124 // Set current_template to context 125 if _, ok := data["current_template"]; !ok { 126 data["current_template"] = templateName 127 } 128 129 source, err := s.resolve(templateName) 130 if err != nil { 131 return "", err 132 } 133 134 helpers := map[string]interface{}{} 135 136 for k, v := range s.Helpers { 137 helpers[k] = v 138 } 139 140 // Allows to specify custom partialFeeder 141 if helpers["partialFeeder"] == nil { 142 helpers["partialFeeder"] = s.partialFeeder 143 } 144 145 helpers = s.addAssetsHelpers(helpers) 146 147 body := string(source) 148 for _, ext := range s.exts(name) { 149 te, ok := s.TemplateEngines[ext] 150 if !ok { 151 logrus.Errorf("could not find a template engine for %s", ext) 152 continue 153 } 154 body, err = te(body, data, helpers) 155 if err != nil { 156 return "", err 157 } 158 } 159 160 return template.HTML(body), nil 161 } 162 163 func (s templateRenderer) localizedName(name string, data Data) string { 164 templateName := name 165 166 languages, ok := data["languages"].([]string) 167 if !ok || len(languages) == 0 { 168 return templateName 169 } 170 171 ll := len(languages) 172 // Default language is the last in the list 173 defaultLanguage := languages[ll-1] 174 ext := filepath.Ext(name) 175 rawName := strings.TrimSuffix(name, ext) 176 177 for _, l := range languages { 178 var candidateName string 179 if l == defaultLanguage { 180 break 181 } 182 183 candidateName = rawName + "." + strings.ToLower(l) + ext 184 if _, err := s.resolve(candidateName); err == nil { 185 // Replace name with the existing suffixed version 186 templateName = candidateName 187 break 188 } 189 } 190 191 return templateName 192 } 193 194 func (s templateRenderer) exts(name string) []string { 195 exts := []string{} 196 for { 197 ext := filepath.Ext(name) 198 if ext == "" { 199 break 200 } 201 name = strings.TrimSuffix(name, ext) 202 exts = append(exts, strings.ToLower(ext[1:])) 203 } 204 if len(exts) == 0 { 205 return []string{"html"} 206 } 207 sort.Sort(sort.Reverse(sort.StringSlice(exts))) 208 return exts 209 } 210 211 func (s templateRenderer) assetPath(file string) (string, error) { 212 213 if len(assetMap.Keys()) == 0 || os.Getenv("GO_ENV") != "production" { 214 manifest, err := s.AssetsBox.FindString("manifest.json") 215 216 if err != nil { 217 manifest, err = s.AssetsBox.FindString("assets/manifest.json") 218 if err != nil { 219 return assetPathFor(file), nil 220 } 221 } 222 223 err = loadManifest(manifest) 224 if err != nil { 225 return assetPathFor(file), fmt.Errorf("your manifest.json is not correct: %s", err) 226 } 227 } 228 229 return assetPathFor(file), nil 230 } 231 232 // Template renders the named files using the specified 233 // content type and the github.com/gobuffalo/plush 234 // package for templating. If more than 1 file is provided 235 // the second file will be considered a "layout" file 236 // and the first file will be the "content" file which will 237 // be placed into the "layout" using "{{yield}}". 238 func Template(c string, names ...string) Renderer { 239 e := New(Options{}) 240 return e.Template(c, names...) 241 } 242 243 // Template renders the named files using the specified 244 // content type and the github.com/gobuffalo/plush 245 // package for templating. If more than 1 file is provided 246 // the second file will be considered a "layout" file 247 // and the first file will be the "content" file which will 248 // be placed into the "layout" using "{{yield}}". 249 func (e *Engine) Template(c string, names ...string) Renderer { 250 return &templateRenderer{ 251 Engine: e, 252 contentType: c, 253 names: names, 254 aliases: syncx.StringMap{}, 255 } 256 }