310 lines
7.9 KiB
Go
310 lines
7.9 KiB
Go
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
|
|
Content template.HTML
|
|
Photos []diaryPhoto
|
|
}
|
|
|
|
type diaryYearData struct{ Months []diaryMonthSummary }
|
|
type diaryMonthData struct{ Days []diaryDaySection }
|
|
type diaryDayData struct{ Photos []diaryPhoto }
|
|
|
|
var diaryYearTmpl = template.Must(template.New("diary-year").Parse(
|
|
`{{range .Months}}<div class="diary-section"><h2 class="diary-heading"><a href="{{.URL}}">{{.Name}}</a>{{if .PhotoCount}} <span class="diary-photo-count">({{.PhotoCount}} photos)</span>{{end}}</h2></div>{{end}}`,
|
|
))
|
|
|
|
var diaryMonthTmpl = template.Must(template.New("diary-month").Parse(
|
|
`{{range .Days}}<div class="diary-section"><h2 class="diary-heading">{{if .URL}}<a href="{{.URL}}">{{.Heading}}</a>{{else}}{{.Heading}}{{end}}</h2>{{if .Content}}<div class="content">{{.Content}}</div>{{end}}{{if .Photos}}<div class="diary-photo-grid">{{range .Photos}}<a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a>{{end}}</div>{{end}}</div>{{end}}`,
|
|
))
|
|
|
|
var diaryDayTmpl = template.Must(template.New("diary-day").Parse(
|
|
`{{if .Photos}}<div class="diary-photo-grid">{{range .Photos}}<a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a>{{end}}</div>{{end}}`,
|
|
))
|
|
|
|
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.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 := date.Format("Monday, January 2")
|
|
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,
|
|
Content: content,
|
|
Photos: photos,
|
|
})
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := diaryMonthTmpl.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.Execute(&buf, diaryDayData{Photos: photos}); err != nil {
|
|
log.Printf("diary day template: %v", err)
|
|
return ""
|
|
}
|
|
return template.HTML(buf.String())
|
|
}
|