Refactoring of calendar widget
This commit is contained in:
@@ -369,7 +369,6 @@ type calendarData struct {
|
||||
DisplayYear int
|
||||
DisplayMonth int
|
||||
DisplayMonthName string // pre-resolved so the template doesn't need arithmetic
|
||||
DisplayMonthURL string
|
||||
DiaryURL string
|
||||
YearURL string
|
||||
Months []calMonthGrid
|
||||
@@ -470,7 +469,7 @@ func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int)
|
||||
Num: m,
|
||||
Name: germanMonths[time.Month(m)],
|
||||
AnchorURL: monthAnchor(displayYear, m),
|
||||
Weeks: buildMonthGrid(displayYear, m, today, cd, hasDayEntryByMonth[m], diaryRootURL, dayAnchor),
|
||||
Weeks: buildMonthGrid(displayYear, m, today, cd, hasDayEntryByMonth[m], dayAnchor),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,7 +505,6 @@ func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int)
|
||||
DisplayYear: displayYear,
|
||||
DisplayMonth: displayMonth,
|
||||
DisplayMonthName: months[displayMonth-1].Name,
|
||||
DisplayMonthURL: months[displayMonth-1].AnchorURL,
|
||||
DiaryURL: diaryRootURL,
|
||||
YearURL: yearURL,
|
||||
Months: months,
|
||||
@@ -523,10 +521,11 @@ func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int)
|
||||
|
||||
// buildMonthGrid renders one month's day cells as a Monday-first week grid.
|
||||
// hasDayEntry maps day-of-month → has a diary entry. dayAnchor produces the
|
||||
// in-page anchor (or full URL when crossing pages). Empty days always link
|
||||
// to the full month/day URL with ?edit so the diary handler can route into
|
||||
// the "insert new day section" editor.
|
||||
func buildMonthGrid(year, month int, today time.Time, currentDay int, hasDayEntry map[int]bool, diaryRootURL string, dayAnchor func(int, int, int) string) [][]calDay {
|
||||
// in-page anchor (or full URL when crossing pages); empty days link to the
|
||||
// same anchor — every day exists on the year page as either a real or
|
||||
// virtual section, so navigation is enough. Page creation happens via the
|
||||
// [edit] button on the heading itself.
|
||||
func buildMonthGrid(year, month int, today time.Time, currentDay int, hasDayEntry map[int]bool, dayAnchor func(int, int, int) string) [][]calDay {
|
||||
firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
|
||||
startOffset := int(firstDay.Weekday()+6) % 7
|
||||
daysInMonth := time.Date(year, time.Month(month)+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
||||
@@ -535,14 +534,10 @@ func buildMonthGrid(year, month int, today time.Time, currentDay int, hasDayEntr
|
||||
week := make([]calDay, 7)
|
||||
col := startOffset
|
||||
for d := 1; d <= daysInMonth; d++ {
|
||||
cell := calDay{Num: d, HasEntry: hasDayEntry[d]}
|
||||
if cell.HasEntry {
|
||||
cell.URL = dayAnchor(year, month, d)
|
||||
} else {
|
||||
cell.URL = path.Join(diaryRootURL,
|
||||
fmt.Sprintf("%d", year),
|
||||
fmt.Sprintf("%02d", month),
|
||||
fmt.Sprintf("%02d", d)) + "/?edit"
|
||||
cell := calDay{
|
||||
Num: d,
|
||||
HasEntry: hasDayEntry[d],
|
||||
URL: dayAnchor(year, month, d),
|
||||
}
|
||||
cell.IsCurrent = currentDay > 0 && d == currentDay
|
||||
cell.IsToday = d == today.Day() &&
|
||||
@@ -669,7 +664,7 @@ func renderDiaryYear(yearFS, yearURL string) template.HTML {
|
||||
photos := yearPhotos(yearFS, yearURL)
|
||||
|
||||
out := buildSectionsForRange(sections, photos, 1, len(sections), yearURL)
|
||||
out = appendOrphanPhotoDays(out, photos, year)
|
||||
out = appendVirtualEntries(out, sections, photos, year, yearURL)
|
||||
return renderDiaryContent(out)
|
||||
}
|
||||
|
||||
@@ -699,79 +694,112 @@ func buildSectionsForRange(sections [][]byte, photos []diaryPhoto, start, end in
|
||||
return out
|
||||
}
|
||||
|
||||
// appendOrphanPhotoDays inserts synthetic day sections (no edit button, no
|
||||
// body) for photo dates not already covered by an explicit day section.
|
||||
// Orphans are interleaved by date with existing *date* day sections only —
|
||||
// non-date level-3 headings (e.g. `### Movies` in a year intro) keep their
|
||||
// original document position. Remaining orphans are appended at the end so
|
||||
// the year file's intro material always stays above the diary entries.
|
||||
func appendOrphanPhotoDays(existing []diarySection, photos []diaryPhoto, year int) []diarySection {
|
||||
covered := map[string]bool{}
|
||||
// appendVirtualEntries inserts virtual month and day sections for every
|
||||
// `## YYYY-MM` / `### YYYY-MM-DD` slot in `year` that lacks a real section.
|
||||
// Virtual day sections carry photos when present. Each virtual entry's
|
||||
// EditURL routes through the insert-before flow so clicking [edit] splices
|
||||
// the section into the year file at the right chronological position.
|
||||
//
|
||||
// Scope: past years get all 12 months / 365(6) days; the current year stops
|
||||
// at today; future years are returned unchanged.
|
||||
//
|
||||
// Interleave: real date sections (`## YYYY-MM`, `### YYYY-MM-DD`) keep their
|
||||
// document position; virtual entries are spliced in lexicographic ID order
|
||||
// before the next real date section. Non-date headings (e.g. `## Events` →
|
||||
// `### Festival` in a year intro) are left where the user wrote them.
|
||||
func appendVirtualEntries(existing []diarySection, sections [][]byte, photos []diaryPhoto, year int, yearURL string) []diarySection {
|
||||
today := time.Now()
|
||||
if year > today.Year() {
|
||||
return existing
|
||||
}
|
||||
|
||||
coveredMonth := map[string]bool{}
|
||||
coveredDay := map[string]bool{}
|
||||
for _, s := range existing {
|
||||
if s.Level == 3 {
|
||||
covered[s.ID] = true
|
||||
switch {
|
||||
case s.Level == 2 && len(s.ID) == 7 && isDateHeading(s.ID):
|
||||
coveredMonth[s.ID] = true
|
||||
case s.Level == 3 && len(s.ID) == 10 && isDateHeading(s.ID):
|
||||
coveredDay[s.ID] = true
|
||||
}
|
||||
}
|
||||
type orphan struct {
|
||||
date time.Time
|
||||
header string
|
||||
photos []diaryPhoto
|
||||
}
|
||||
orphMap := map[string]*orphan{}
|
||||
|
||||
photoByDay := map[string][]diaryPhoto{}
|
||||
for _, p := range photos {
|
||||
if p.Date.Year() != year {
|
||||
continue
|
||||
}
|
||||
key := p.Date.Format("2006-01-02")
|
||||
if covered[key] {
|
||||
continue
|
||||
}
|
||||
o, ok := orphMap[key]
|
||||
if !ok {
|
||||
o = &orphan{date: p.Date, header: key}
|
||||
orphMap[key] = o
|
||||
}
|
||||
o.photos = append(o.photos, p)
|
||||
photoByDay[p.Date.Format("2006-01-02")] = append(photoByDay[p.Date.Format("2006-01-02")], p)
|
||||
}
|
||||
if len(orphMap) == 0 {
|
||||
|
||||
lastDay := time.Date(year, time.December, 31, 0, 0, 0, 0, time.UTC)
|
||||
if year == today.Year() {
|
||||
lastDay = time.Date(year, today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
var virtual []diarySection
|
||||
for d := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC); !d.After(lastDay); d = d.AddDate(0, 0, 1) {
|
||||
if d.Day() == 1 {
|
||||
monthID := d.Format("2006-01")
|
||||
if !coveredMonth[monthID] {
|
||||
idx := computeInsertIndex(sections, monthID)
|
||||
virtual = append(virtual, diarySection{
|
||||
Level: 2,
|
||||
ID: monthID,
|
||||
Heading: monthID,
|
||||
EditURL: fmt.Sprintf("%s?edit&insert_before=%d&heading=%s&level=%s",
|
||||
yearURL, idx, url.QueryEscape(monthID), url.QueryEscape("##")),
|
||||
})
|
||||
}
|
||||
}
|
||||
dayID := d.Format("2006-01-02")
|
||||
if !coveredDay[dayID] {
|
||||
idx := computeInsertIndex(sections, dayID)
|
||||
virtual = append(virtual, diarySection{
|
||||
Level: 3,
|
||||
ID: dayID,
|
||||
Heading: dayID,
|
||||
EditURL: fmt.Sprintf("%s?edit&insert_before=%d&heading=%s",
|
||||
yearURL, idx, url.QueryEscape(dayID)),
|
||||
Photos: photoByDay[dayID],
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(virtual) == 0 {
|
||||
return existing
|
||||
}
|
||||
orphans := make([]*orphan, 0, len(orphMap))
|
||||
for _, o := range orphMap {
|
||||
orphans = append(orphans, o)
|
||||
}
|
||||
sort.Slice(orphans, func(i, j int) bool { return orphans[i].date.Before(orphans[j].date) })
|
||||
|
||||
out := make([]diarySection, 0, len(existing)+len(orphans))
|
||||
oi := 0
|
||||
out := make([]diarySection, 0, len(existing)+len(virtual))
|
||||
vi := 0
|
||||
for _, s := range existing {
|
||||
if s.Level == 3 {
|
||||
if _, _, _, ok := parseISODate(s.ID); ok {
|
||||
for oi < len(orphans) && orphans[oi].header < s.ID {
|
||||
out = append(out, diarySection{
|
||||
Level: 3,
|
||||
ID: orphans[oi].header,
|
||||
Heading: orphans[oi].header,
|
||||
Photos: orphans[oi].photos,
|
||||
})
|
||||
oi++
|
||||
}
|
||||
if isRealDateSection(s) {
|
||||
for vi < len(virtual) && virtual[vi].ID < s.ID {
|
||||
out = append(out, virtual[vi])
|
||||
vi++
|
||||
}
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
for oi < len(orphans) {
|
||||
out = append(out, diarySection{
|
||||
Level: 3,
|
||||
ID: orphans[oi].header,
|
||||
Heading: orphans[oi].header,
|
||||
Photos: orphans[oi].photos,
|
||||
})
|
||||
oi++
|
||||
for vi < len(virtual) {
|
||||
out = append(out, virtual[vi])
|
||||
vi++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// isRealDateSection reports whether a rendered diarySection is one of the
|
||||
// date-headed slots (`## YYYY-MM` or `### YYYY-MM-DD`) the virtual-entry
|
||||
// interleave sorts against.
|
||||
func isRealDateSection(s diarySection) bool {
|
||||
switch s.Level {
|
||||
case 2:
|
||||
return len(s.ID) == 7 && isDateHeading(s.ID)
|
||||
case 3:
|
||||
return len(s.ID) == 10 && isDateHeading(s.ID)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseISODate parses "YYYY-MM-DD" leading characters of s. Returns ok=false
|
||||
// if the prefix does not match.
|
||||
func parseISODate(s string) (year, month, day int, ok bool) {
|
||||
|
||||
Reference in New Issue
Block a user