tangled
alpha
login
or
join now
back
round
0
view raw
appview/pages: parse fragments in tree
#534
merged
opened by
oppi.li
4 months ago
targeting
master
from
push-mvmrzuxwmzvs
this allows fragments to reference each other.
Signed-off-by: oppiliappan
me@oppi.li
options
unified
split
Changed files
+51
-94
appview
pages
pages.go
+51
-94
appview/pages/pages.go
···
9
"html/template"
10
"io"
11
"io/fs"
12
-
"log"
13
"net/http"
14
"os"
15
"path/filepath"
···
48
avatar config.AvatarConfig
49
resolver *idresolver.Resolver
50
dev bool
51
-
embedFS embed.FS
52
templateDir string // Path to templates on disk for dev mode
53
rctx *markup.RenderContext
0
54
}
55
56
func NewPages(config *config.Config, res *idresolver.Resolver) *Pages {
···
67
t: make(map[string]*template.Template),
68
dev: config.Core.Dev,
69
avatar: config.Avatar,
70
-
embedFS: Files,
71
rctx: rctx,
72
resolver: res,
73
templateDir: "appview/pages",
0
74
}
75
76
// Initial load of all templates
···
79
return p
80
}
81
82
-
func (p *Pages) loadAllTemplates() {
83
-
templates := make(map[string]*template.Template)
84
var fragmentPaths []string
85
-
86
-
// Use embedded FS for initial loading
87
-
// First, collect all fragment paths
88
err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
89
if err != nil {
90
return err
···
98
if !strings.Contains(path, "fragments/") {
99
return nil
100
}
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
fragmentPaths = append(fragmentPaths, path)
111
-
log.Printf("loaded fragment: %s", name)
112
return nil
113
})
114
if err != nil {
115
-
log.Fatalf("walking template dir for fragments: %v", err)
0
0
0
0
0
0
0
0
0
0
116
}
117
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
118
// Then walk through and setup the rest of the templates
119
err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
120
if err != nil {
···
148
return fmt.Errorf("setting up template: %w", err)
149
}
150
templates[name] = tmpl
151
-
log.Printf("loaded template: %s", name)
152
return nil
153
})
154
if err != nil {
155
-
log.Fatalf("walking template dir: %v", err)
0
156
}
157
158
-
log.Printf("total templates loaded: %d", len(templates))
159
p.mu.Lock()
160
defer p.mu.Unlock()
161
p.t = templates
162
}
163
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
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
230
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
-
}
235
}
236
237
p.mu.RLock()
···
1269
1270
sub, err := fs.Sub(Files, "static")
1271
if err != nil {
1272
-
log.Fatalf("no static dir found? that's crazy: %v", err)
0
1273
}
1274
// Custom handler to apply Cache-Control headers for font files
1275
return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
···
1292
func CssContentHash() string {
1293
cssFile, err := Files.Open("static/tw.css")
1294
if err != nil {
1295
-
log.Printf("Error opening CSS file: %v", err)
1296
return ""
1297
}
1298
defer cssFile.Close()
1299
1300
hasher := sha256.New()
1301
if _, err := io.Copy(hasher, cssFile); err != nil {
1302
-
log.Printf("Error hashing CSS file: %v", err)
1303
return ""
1304
}
1305
···
9
"html/template"
10
"io"
11
"io/fs"
12
+
"log/slog"
13
"net/http"
14
"os"
15
"path/filepath"
···
48
avatar config.AvatarConfig
49
resolver *idresolver.Resolver
50
dev bool
51
+
embedFS fs.FS
52
templateDir string // Path to templates on disk for dev mode
53
rctx *markup.RenderContext
54
+
logger *slog.Logger
55
}
56
57
func NewPages(config *config.Config, res *idresolver.Resolver) *Pages {
···
68
t: make(map[string]*template.Template),
69
dev: config.Core.Dev,
70
avatar: config.Avatar,
0
71
rctx: rctx,
72
resolver: res,
73
templateDir: "appview/pages",
74
+
logger: slog.Default().With("component", "pages"),
75
}
76
77
// Initial load of all templates
···
80
return p
81
}
82
83
+
func (p *Pages) fragmentPaths() ([]string, error) {
0
84
var fragmentPaths []string
0
0
0
85
err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
86
if err != nil {
87
return err
···
95
if !strings.Contains(path, "fragments/") {
96
return nil
97
}
0
0
0
0
0
0
0
0
0
98
fragmentPaths = append(fragmentPaths, path)
0
99
return nil
100
})
101
if err != nil {
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
121
+
}
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
+
}
140
// Then walk through and setup the rest of the templates
141
err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
142
if err != nil {
···
170
return fmt.Errorf("setting up template: %w", err)
171
}
172
templates[name] = tmpl
173
+
l.Debug("loaded all templates")
174
return nil
175
})
176
if err != nil {
177
+
l.Error("walking template dir", "err", err)
178
+
panic(err)
179
}
180
181
+
l.Info("loaded all templates", "total", len(templates))
182
p.mu.Lock()
183
defer p.mu.Unlock()
184
p.t = templates
185
}
186
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
187
func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error {
188
+
// In dev mode, reparse templates from disk before executing
189
if p.dev {
190
+
p.loadAllTemplates()
0
0
0
191
}
192
193
p.mu.RLock()
···
1225
1226
sub, err := fs.Sub(Files, "static")
1227
if err != nil {
1228
+
p.logger.Error("no static dir found? that's crazy", "err", err)
1229
+
panic(err)
1230
}
1231
// Custom handler to apply Cache-Control headers for font files
1232
return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
···
1249
func CssContentHash() string {
1250
cssFile, err := Files.Open("static/tw.css")
1251
if err != nil {
1252
+
slog.Debug("Error opening CSS file", "err", err)
1253
return ""
1254
}
1255
defer cssFile.Close()
1256
1257
hasher := sha256.New()
1258
if _, err := io.Copy(hasher, cssFile); err != nil {
1259
+
slog.Debug("Error hashing CSS file", "err", err)
1260
return ""
1261
}
1262