LabCodeHub öffentliche Ansicht
Anmelden
Küpper / Quildrop öffentlich
Branch: main
Quildrop / internal / templates / render.go
Verlauf Rohdaten
R Rüdiger Küpper fix: new version with themes + multi arch builds
b56ed7e0 vor 14 Tagen
internal/templates/render.go 283 Zeilen · 6.5 KB · Go
1
package templates
2
3
import (
4
	"bytes"
5
	"fmt"
6
	"html/template"
7
	"io"
8
	"os"
9
	"path/filepath"
10
	"strings"
11
	"time"
12
13
	"github.com/ruedigerp/newblog/internal/config"
14
	"github.com/ruedigerp/newblog/internal/content"
15
)
16
17
type SiteData struct {
18
	Title       string
19
	Description string
20
	Author      string
21
	BaseURL     string
22
	Menu        []config.MenuItem
23
}
24
25
type HomeData struct {
26
	Site       SiteData
27
	Posts      []*content.Post
28
	Page       int // current page (1-based)
29
	TotalPages int
30
}
31
32
type PostData struct {
33
	Site     SiteData
34
	Post     *content.Post
35
	PrevPost *content.Post // newer post (nil if this is the newest)
36
	NextPost *content.Post // older post (nil if this is the oldest)
37
}
38
39
type TagsData struct {
40
	Site SiteData
41
	Tags map[string]int
42
}
43
44
type TagData struct {
45
	Site  SiteData
46
	Tag   string
47
	Posts []*content.Post
48
}
49
50
type CategoriesData struct {
51
	Site       SiteData
52
	Categories map[string]int
53
}
54
55
type CategoryData struct {
56
	Site     SiteData
57
	Category string
58
	Posts    []*content.Post
59
}
60
61
type PageData struct {
62
	Site SiteData
63
	Page *content.Page
64
}
65
66
var funcMap = template.FuncMap{
67
	"formatDate": func(t time.Time) string {
68
		months := []string{
69
			"", "Januar", "Februar", "März", "April", "Mai", "Juni",
70
			"Juli", "August", "September", "Oktober", "November", "Dezember",
71
		}
72
		return t.Format("02.") + " " + months[t.Month()] + " " + t.Format("2006")
73
	},
74
	"isoDate": func(t time.Time) string {
75
		return t.Format("2006-01-02")
76
	},
77
	"rssDate": func(t time.Time) string {
78
		return t.Format(time.RFC1123Z)
79
	},
80
	"lower": strings.ToLower,
81
	"safeHTML": func(s template.HTML) template.HTML {
82
		return s
83
	},
84
	"hasChildren": func(item config.MenuItem) bool {
85
		return len(item.Children) > 0
86
	},
87
	"add": func(a, b int) int {
88
		return a + b
89
	},
90
	"sub": func(a, b int) int {
91
		return a - b
92
	},
93
	"pageURL": func(page int) string {
94
		if page <= 1 {
95
			return "/"
96
		}
97
		return fmt.Sprintf("/page/%d/", page)
98
	},
99
	"seq": func(from, to int) []int {
100
		var s []int
101
		for i := from; i <= to; i++ {
102
			s = append(s, i)
103
		}
104
		return s
105
	},
106
	// paginationRange returns page numbers to display, with -1 as ellipsis placeholder.
107
	// Shows: first, last, and a window of 2 around the current page.
108
	"paginationRange": func(current, total int) []int {
109
		if total <= 7 {
110
			// Show all pages if 7 or fewer
111
			var s []int
112
			for i := 1; i <= total; i++ {
113
				s = append(s, i)
114
			}
115
			return s
116
		}
117
		pages := make(map[int]bool)
118
		pages[1] = true
119
		pages[total] = true
120
		for i := current - 2; i <= current+2; i++ {
121
			if i >= 1 && i <= total {
122
				pages[i] = true
123
			}
124
		}
125
		var result []int
126
		prev := 0
127
		for i := 1; i <= total; i++ {
128
			if pages[i] {
129
				if prev > 0 && i-prev > 1 {
130
					result = append(result, -1) // ellipsis
131
				}
132
				result = append(result, i)
133
				prev = i
134
			}
135
		}
136
		return result
137
	},
138
	"isEllipsis": func(n int) bool {
139
		return n == -1
140
	},
141
}
142
143
var (
144
	homeTmpl       *view
145
	postTmpl       *view
146
	tagsTmpl       *view
147
	tagTmpl        *view
148
	categoriesTmpl *view
149
	categoryTmpl   *view
150
	pageTmpl       *view
151
)
152
153
// templateDir holds the directory the templates of the active theme were
154
// loaded from (e.g. "themes/default/templates").
155
var templateDir string
156
157
// devMode re-parses templates on every render, so theme edits become visible
158
// without restarting the server.
159
var devMode bool
160
161
// SetDevMode enables or disables template live reload. Must be called before
162
// serving requests.
163
func SetDevMode(enabled bool) {
164
	devMode = enabled
165
}
166
167
// DevMode reports whether template live reload is enabled.
168
func DevMode() bool {
169
	return devMode
170
}
171
172
// view is a page template together with the theme files it was built from,
173
// so it can be re-parsed on demand in dev mode.
174
type view struct {
175
	files []string
176
	tmpl  *template.Template
177
}
178
179
func newView(files ...string) (*view, error) {
180
	tmpl, err := parseTemplate(files...)
181
	if err != nil {
182
		return nil, err
183
	}
184
	return &view{files: files, tmpl: tmpl}, nil
185
}
186
187
func (v *view) execute(w io.Writer, data any) error {
188
	if !devMode {
189
		return v.tmpl.ExecuteTemplate(w, "base", data)
190
	}
191
	// Live reload: parse the theme files again and render into a buffer first,
192
	// so a broken template does not emit half a page.
193
	tmpl, err := parseTemplate(v.files...)
194
	if err != nil {
195
		return err
196
	}
197
	var buf bytes.Buffer
198
	if err := tmpl.ExecuteTemplate(&buf, "base", data); err != nil {
199
		return err
200
	}
201
	_, err = w.Write(buf.Bytes())
202
	return err
203
}
204
205
func parseTemplate(files ...string) (*template.Template, error) {
206
	paths := make([]string, 0, len(files))
207
	for _, f := range files {
208
		path := filepath.Join(templateDir, f)
209
		if _, err := os.Stat(path); err != nil {
210
			return nil, fmt.Errorf("missing template %s in theme: %w", f, err)
211
		}
212
		paths = append(paths, path)
213
	}
214
	return template.New("").Funcs(funcMap).ParseFiles(paths...)
215
}
216
217
// Init loads all templates of the theme located in themeDir
218
// (e.g. "themes/default"). The templates are expected in themeDir/templates.
219
func Init(themeDir string) error {
220
	templateDir = filepath.Join(themeDir, "templates")
221
	info, err := os.Stat(templateDir)
222
	if err != nil || !info.IsDir() {
223
		return fmt.Errorf("theme templates not found in %s (check 'theme' and 'themesDir' in config.yaml)", templateDir)
224
	}
225
226
	homeTmpl, err = newView("base.html", "home.html")
227
	if err != nil {
228
		return fmt.Errorf("home template: %w", err)
229
	}
230
	postTmpl, err = newView("base.html", "post.html")
231
	if err != nil {
232
		return fmt.Errorf("post template: %w", err)
233
	}
234
	tagsTmpl, err = newView("base.html", "tags.html")
235
	if err != nil {
236
		return fmt.Errorf("tags template: %w", err)
237
	}
238
	tagTmpl, err = newView("base.html", "tag.html")
239
	if err != nil {
240
		return fmt.Errorf("tag template: %w", err)
241
	}
242
	categoriesTmpl, err = newView("base.html", "categories.html")
243
	if err != nil {
244
		return fmt.Errorf("categories template: %w", err)
245
	}
246
	categoryTmpl, err = newView("base.html", "category.html")
247
	if err != nil {
248
		return fmt.Errorf("category template: %w", err)
249
	}
250
	pageTmpl, err = newView("base.html", "page.html")
251
	if err != nil {
252
		return fmt.Errorf("page template: %w", err)
253
	}
254
	return nil
255
}
256
257
func RenderHome(w io.Writer, data HomeData) error {
258
	return homeTmpl.execute(w, data)
259
}
260
261
func RenderPost(w io.Writer, data PostData) error {
262
	return postTmpl.execute(w, data)
263
}
264
265
func RenderTags(w io.Writer, data TagsData) error {
266
	return tagsTmpl.execute(w, data)
267
}
268
269
func RenderTag(w io.Writer, data TagData) error {
270
	return tagTmpl.execute(w, data)
271
}
272
273
func RenderCategories(w io.Writer, data CategoriesData) error {
274
	return categoriesTmpl.execute(w, data)
275
}
276
277
func RenderCategory(w io.Writer, data CategoryData) error {
278
	return categoryTmpl.execute(w, data)
279
}
280
281
func RenderPage(w io.Writer, data PageData) error {
282
	return pageTmpl.execute(w, data)
283
}