Monorepo for Tangled tangled.org

appview/pages: parse fragments in tree

this allows fragments to reference each other.

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 8a5602cf 0c5d93e7

verified
Changed files
+51 -94
appview
pages
+51 -94
appview/pages/pages.go
··· 9 9 "html/template" 10 10 "io" 11 11 "io/fs" 12 - "log" 12 + "log/slog" 13 13 "net/http" 14 14 "os" 15 15 "path/filepath" ··· 48 48 avatar config.AvatarConfig 49 49 resolver *idresolver.Resolver 50 50 dev bool 51 - embedFS embed.FS 51 + embedFS fs.FS 52 52 templateDir string // Path to templates on disk for dev mode 53 53 rctx *markup.RenderContext 54 + logger *slog.Logger 54 55 } 55 56 56 57 func NewPages(config *config.Config, res *idresolver.Resolver) *Pages { ··· 67 68 t: make(map[string]*template.Template), 68 69 dev: config.Core.Dev, 69 70 avatar: config.Avatar, 70 - embedFS: Files, 71 71 rctx: rctx, 72 72 resolver: res, 73 73 templateDir: "appview/pages", 74 + logger: slog.Default().With("component", "pages"), 74 75 } 75 76 76 77 // Initial load of all templates ··· 79 80 return p 80 81 } 81 82 82 - func (p *Pages) loadAllTemplates() { 83 - templates := make(map[string]*template.Template) 83 + func (p *Pages) fragmentPaths() ([]string, error) { 84 84 var fragmentPaths []string 85 - 86 - // Use embedded FS for initial loading 87 - // First, collect all fragment paths 88 85 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 89 86 if err != nil { 90 87 return err ··· 98 95 if !strings.Contains(path, "fragments/") { 99 96 return nil 100 97 } 101 - name := strings.TrimPrefix(path, "templates/") 102 - name = strings.TrimSuffix(name, ".html") 103 - tmpl, err := template.New(name). 104 - Funcs(p.funcMap()). 105 - ParseFS(p.embedFS, path) 106 - if err != nil { 107 - log.Fatalf("setting up fragment: %v", err) 108 - } 109 - templates[name] = tmpl 110 98 fragmentPaths = append(fragmentPaths, path) 111 - log.Printf("loaded fragment: %s", name) 112 99 return nil 113 100 }) 114 101 if err != nil { 115 - log.Fatalf("walking template dir for fragments: %v", err) 102 + return nil, err 103 + } 104 + 105 + return fragmentPaths, nil 106 + } 107 + 108 + func (p *Pages) loadAllTemplates() { 109 + if p.dev { 110 + p.embedFS = os.DirFS(p.templateDir) 111 + } else { 112 + p.embedFS = Files 113 + } 114 + 115 + l := p.logger.With("handler", "loadAllTemplates") 116 + templates := make(map[string]*template.Template) 117 + fragmentPaths, err := p.fragmentPaths() 118 + if err != nil { 119 + l.Error("failed to collect fragments", "err", err) 120 + return 116 121 } 117 122 123 + // parse all fragments together 124 + allFragments := template.New("").Funcs(p.funcMap()) 125 + for _, f := range fragmentPaths { 126 + name := strings.TrimPrefix(f, "templates/") 127 + name = strings.TrimSuffix(name, ".html") 128 + pf, err := template.New(name).Funcs(p.funcMap()).ParseFS(p.embedFS, f) 129 + if err != nil { 130 + l.Error("failed to parse fragment", "name", name, "path", f) 131 + return 132 + } 133 + allFragments, err = allFragments.AddParseTree(name, pf.Tree) 134 + if err != nil { 135 + l.Error("failed to add parse tree", "name", name, "path", f) 136 + return 137 + } 138 + templates[name] = allFragments.Lookup(name) 139 + } 118 140 // Then walk through and setup the rest of the templates 119 141 err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 120 142 if err != nil { ··· 148 170 return fmt.Errorf("setting up template: %w", err) 149 171 } 150 172 templates[name] = tmpl 151 - log.Printf("loaded template: %s", name) 173 + l.Debug("loaded all templates") 152 174 return nil 153 175 }) 154 176 if err != nil { 155 - log.Fatalf("walking template dir: %v", err) 177 + l.Error("walking template dir", "err", err) 178 + panic(err) 156 179 } 157 180 158 - log.Printf("total templates loaded: %d", len(templates)) 181 + l.Info("loaded all templates", "total", len(templates)) 159 182 p.mu.Lock() 160 183 defer p.mu.Unlock() 161 184 p.t = templates 162 185 } 163 186 164 - // loadTemplateFromDisk loads a template from the filesystem in dev mode 165 - func (p *Pages) loadTemplateFromDisk(name string) error { 166 - if !p.dev { 167 - return nil 168 - } 169 - 170 - log.Printf("reloading template from disk: %s", name) 171 - 172 - // Find all fragments first 173 - var fragmentPaths []string 174 - err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { 175 - if err != nil { 176 - return err 177 - } 178 - if d.IsDir() { 179 - return nil 180 - } 181 - if !strings.HasSuffix(path, ".html") { 182 - return nil 183 - } 184 - if !strings.Contains(path, "fragments/") { 185 - return nil 186 - } 187 - fragmentPaths = append(fragmentPaths, path) 188 - return nil 189 - }) 190 - if err != nil { 191 - return fmt.Errorf("walking disk template dir for fragments: %w", err) 192 - } 193 - 194 - // Find the template path on disk 195 - templatePath := filepath.Join(p.templateDir, "templates", name+".html") 196 - if _, err := os.Stat(templatePath); os.IsNotExist(err) { 197 - return fmt.Errorf("template not found on disk: %s", name) 198 - } 199 - 200 - // Create a new template 201 - tmpl := template.New(name).Funcs(p.funcMap()) 202 - 203 - // Parse layouts 204 - layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") 205 - layouts, err := filepath.Glob(layoutGlob) 206 - if err != nil { 207 - return fmt.Errorf("finding layout templates: %w", err) 208 - } 209 - 210 - // Create paths for parsing 211 - allFiles := append(layouts, fragmentPaths...) 212 - allFiles = append(allFiles, templatePath) 213 - 214 - // Parse all templates 215 - tmpl, err = tmpl.ParseFiles(allFiles...) 216 - if err != nil { 217 - return fmt.Errorf("parsing template files: %w", err) 218 - } 219 - 220 - // Update the template in the map 221 - p.mu.Lock() 222 - defer p.mu.Unlock() 223 - p.t[name] = tmpl 224 - log.Printf("template reloaded from disk: %s", name) 225 - return nil 226 - } 227 - 228 187 func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 229 - // In dev mode, reload the template from disk before executing 188 + // In dev mode, reparse templates from disk before executing 230 189 if p.dev { 231 - if err := p.loadTemplateFromDisk(templateName); err != nil { 232 - log.Printf("warning: failed to reload template %s from disk: %v", templateName, err) 233 - // Continue with the existing template 234 - } 190 + p.loadAllTemplates() 235 191 } 236 192 237 193 p.mu.RLock() ··· 1269 1225 1270 1226 sub, err := fs.Sub(Files, "static") 1271 1227 if err != nil { 1272 - log.Fatalf("no static dir found? that's crazy: %v", err) 1228 + p.logger.Error("no static dir found? that's crazy", "err", err) 1229 + panic(err) 1273 1230 } 1274 1231 // Custom handler to apply Cache-Control headers for font files 1275 1232 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) ··· 1292 1249 func CssContentHash() string { 1293 1250 cssFile, err := Files.Open("static/tw.css") 1294 1251 if err != nil { 1295 - log.Printf("Error opening CSS file: %v", err) 1252 + slog.Debug("Error opening CSS file", "err", err) 1296 1253 return "" 1297 1254 } 1298 1255 defer cssFile.Close() 1299 1256 1300 1257 hasher := sha256.New() 1301 1258 if _, err := io.Copy(hasher, cssFile); err != nil { 1302 - log.Printf("Error hashing CSS file: %v", err) 1259 + slog.Debug("Error hashing CSS file", "err", err) 1303 1260 return "" 1304 1261 } 1305 1262