package main import ( "bytes" "fmt" "html/template" "log" "net/url" "os" "path" "path/filepath" "sort" "strconv" "strings" "time" ) func init() { pageTypeHandlers = append(pageTypeHandlers, &diaryHandler{}) } type diaryHandler struct{} func (d *diaryHandler) handle(root, fsPath, urlPath string) *specialPage { depth, ok := findDiaryContext(root, fsPath) if !ok || depth == 0 { return nil } var content template.HTML switch depth { case 1: content = renderDiaryYear(fsPath, urlPath) case 2: content = renderDiaryMonth(fsPath, urlPath) case 3: content = renderDiaryDay(fsPath, urlPath) } return &specialPage{Content: content, SuppressListing: true} } // findDiaryContext walks up from fsPath toward root looking for a // .page-settings file with type=diary. Returns the depth of fsPath // relative to the diary root, and whether one was found. // depth=0 means fsPath itself is the diary root. func findDiaryContext(root, fsPath string) (int, bool) { current := fsPath for depth := 0; ; depth++ { s := readPageSettings(current) if s != nil && s.Type == "diary" { return depth, true } if current == root { break } parent := filepath.Dir(current) if parent == current { break } current = parent } return 0, false } // diaryPhoto is a photo file whose name starts with a YYYY-MM-DD date prefix. type diaryPhoto struct { Date time.Time Name string URL string } type diaryMonthSummary struct { Name string URL string PhotoCount int } type diaryDaySection struct { Heading string URL string EditURL string Content template.HTML Photos []diaryPhoto } type diaryYearData struct{ Months []diaryMonthSummary } type diaryMonthData struct{ Days []diaryDaySection } type diaryDayData struct{ Photos []diaryPhoto } var diaryYearTmpl = newTemplate("diary-year.html", "assets/diary/diary-year.html") var diaryMonthTmpl = newTemplate("diary-month.html", "assets/diary/diary-month.html") var diaryDayTmpl = newTemplate("diary-day.html", "assets/diary/diary-day.html") var germanWeekdays = map[time.Weekday]string{ time.Sunday: "Sonntag", time.Monday: "Montag", time.Tuesday: "Dienstag", time.Wednesday: "Mittwoch", time.Thursday: "Donnerstag", time.Friday: "Freitag", time.Saturday: "Samstag", } var germanMonths = map[time.Month]string{ time.January: "Januar", time.February: "Februar", time.March: "März", time.April: "April", time.May: "Mai", time.June: "Juni", time.July: "Juli", time.August: "August", time.September: "September", time.October: "Oktober", time.November: "November", time.December: "Dezember", } func formatGermanDate(t time.Time) string { return fmt.Sprintf("%s, %d. %s %d", germanWeekdays[t.Weekday()], t.Day(), germanMonths[t.Month()], t.Year(), ) } var photoExts = map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, } // yearPhotos returns all photos in yearFsPath whose filename starts with // a YYYY-MM-DD date prefix. func yearPhotos(yearFsPath, yearURLPath string) []diaryPhoto { entries, err := os.ReadDir(yearFsPath) if err != nil { return nil } var photos []diaryPhoto for _, e := range entries { if e.IsDir() { continue } name := e.Name() if !photoExts[strings.ToLower(filepath.Ext(name))] { continue } if len(name) < 10 { continue } t, err := time.Parse("2006-01-02", name[:10]) if err != nil { continue } photos = append(photos, diaryPhoto{ Date: t, Name: name, URL: path.Join(yearURLPath, url.PathEscape(name)), }) } return photos } // renderDiaryYear renders month sections with photo counts for a year folder. func renderDiaryYear(fsPath, urlPath string) template.HTML { year, err := strconv.Atoi(filepath.Base(fsPath)) if err != nil { return "" } photos := yearPhotos(fsPath, urlPath) entries, err := os.ReadDir(fsPath) if err != nil { return "" } var months []diaryMonthSummary for _, e := range entries { if !e.IsDir() { continue } monthNum, err := strconv.Atoi(e.Name()) if err != nil || monthNum < 1 || monthNum > 12 { continue } count := 0 for _, p := range photos { if p.Date.Year() == year && int(p.Date.Month()) == monthNum { count++ } } monthDate := time.Date(year, time.Month(monthNum), 1, 0, 0, 0, 0, time.UTC) months = append(months, diaryMonthSummary{ Name: monthDate.Format("January 2006"), URL: path.Join(urlPath, e.Name()) + "/", PhotoCount: count, }) } var buf bytes.Buffer if err := diaryYearTmpl.get().Execute(&buf, diaryYearData{Months: months}); err != nil { log.Printf("diary year template: %v", err) return "" } return template.HTML(buf.String()) } // renderDiaryMonth renders a section per day, each with its markdown content // and photos sourced from the parent year folder. func renderDiaryMonth(fsPath, urlPath string) template.HTML { yearFsPath := filepath.Dir(fsPath) yearURLPath := parentURL(urlPath) year, err := strconv.Atoi(filepath.Base(yearFsPath)) if err != nil { return "" } monthNum, err := strconv.Atoi(filepath.Base(fsPath)) if err != nil || monthNum < 1 || monthNum > 12 { return "" } allPhotos := yearPhotos(yearFsPath, yearURLPath) var monthPhotos []diaryPhoto for _, p := range allPhotos { if p.Date.Year() == year && int(p.Date.Month()) == monthNum { monthPhotos = append(monthPhotos, p) } } // Collect day numbers from subdirectories and from photo filenames. daySet := map[int]bool{} dayDirs := map[int]string{} // day number → actual directory name entries, _ := os.ReadDir(fsPath) for _, e := range entries { if !e.IsDir() { continue } d, err := strconv.Atoi(e.Name()) if err != nil || d < 1 || d > 31 { continue } daySet[d] = true dayDirs[d] = e.Name() } for _, p := range monthPhotos { daySet[p.Date.Day()] = true } days := make([]int, 0, len(daySet)) for d := range daySet { days = append(days, d) } sort.Ints(days) var sections []diaryDaySection for _, dayNum := range days { date := time.Date(year, time.Month(monthNum), dayNum, 0, 0, 0, 0, time.UTC) heading := formatGermanDate(date) dayURL := path.Join(urlPath, fmt.Sprintf("%02d", dayNum)) + "/" var content template.HTML if dirName, ok := dayDirs[dayNum]; ok { dayURL = path.Join(urlPath, dirName) + "/" dayFsPath := filepath.Join(fsPath, dirName) if raw, err := os.ReadFile(filepath.Join(dayFsPath, "index.md")); err == nil && len(raw) > 0 { if h := extractFirstHeading(raw); h != "" { heading = h raw = stripFirstHeading(raw) } content = renderMarkdown(raw) } } var photos []diaryPhoto for _, p := range monthPhotos { if p.Date.Day() == dayNum { photos = append(photos, p) } } sections = append(sections, diaryDaySection{ Heading: heading, URL: dayURL, EditURL: dayURL + "?edit", Content: content, Photos: photos, }) } var buf bytes.Buffer if err := diaryMonthTmpl.get().Execute(&buf, diaryMonthData{Days: sections}); err != nil { log.Printf("diary month template: %v", err) return "" } return template.HTML(buf.String()) } // renderDiaryDay renders the photo grid for a single day, sourcing photos // from the grandparent year folder. func renderDiaryDay(fsPath, urlPath string) template.HTML { monthFsPath := filepath.Dir(fsPath) yearFsPath := filepath.Dir(monthFsPath) yearURLPath := parentURL(parentURL(urlPath)) year, err := strconv.Atoi(filepath.Base(yearFsPath)) if err != nil { return "" } monthNum, err := strconv.Atoi(filepath.Base(monthFsPath)) if err != nil { return "" } dayNum, err := strconv.Atoi(filepath.Base(fsPath)) if err != nil { return "" } allPhotos := yearPhotos(yearFsPath, yearURLPath) var photos []diaryPhoto for _, p := range allPhotos { if p.Date.Year() == year && int(p.Date.Month()) == monthNum && p.Date.Day() == dayNum { photos = append(photos, p) } } if len(photos) == 0 { return "" } var buf bytes.Buffer if err := diaryDayTmpl.get().Execute(&buf, diaryDayData{Photos: photos}); err != nil { log.Printf("diary day template: %v", err) return "" } return template.HTML(buf.String()) }