diff options
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/generate/calendar.go | 122 | ||||
-rw-r--r-- | cmd/generate/imworkingon.html.tmpl | 72 | ||||
-rw-r--r-- | cmd/generate/main.go | 40 | ||||
-rw-r--r-- | cmd/generate/src_mastodon.go | 52 |
4 files changed, 283 insertions, 3 deletions
diff --git a/cmd/generate/calendar.go b/cmd/generate/calendar.go new file mode 100644 index 0000000..29c3318 --- /dev/null +++ b/cmd/generate/calendar.go @@ -0,0 +1,122 @@ +package main + +import ( + "time" +) + +////////////////////////////////////////////////////////////////////// + +type Date struct { + Year int + Month time.Month + Day int +} + +func DateOf(t time.Time) Date { + y, m, d := t.Date() + return Date{Year: y, Month: m, Day: d} +} + +func (d Date) Time() time.Time { + return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, time.Local) +} + +func (d Date) AddDays(delta int) Date { + return DateOf(d.Time().AddDate(0, 0, delta)) +} + +func (d Date) Weekday() time.Weekday { + return d.Time().Weekday() +} + +func (a Date) Cmp(b Date) int { + switch { + case a.Year < b.Year: + return -1 + case a.Year > b.Year: + return 1 + } + switch { + case a.Month < b.Month: + return -1 + case a.Month > b.Month: + return 1 + } + switch { + case a.Day < b.Day: + return -1 + case a.Day > b.Day: + return 1 + } + return 0 +} + +////////////////////////////////////////////////////////////////////// + +type CalendarDay[T any] struct { + Date + Data T +} + +////////////////////////////////////////////////////////////////////// + +// keyed by time.Weekday +type CalendarWeek[T any] [7]CalendarDay[T] + +////////////////////////////////////////////////////////////////////// + +// must be sorted, must be non-sparse +type Calendar[T any] []CalendarWeek[T] + +func (c Calendar[T]) NumWeekdaysInMonth(weekday time.Weekday, target Date) int { + num := 0 + for _, w := range c { + if w[weekday].Date == (Date{}) { + continue + } + switch { + case w[weekday].Year == target.Year: + switch { + case w[weekday].Month == target.Month: + num++ + case w[weekday].Month > target.Month: + return num + } + case w[weekday].Year > target.Year: + return num + } + } + return num +} + +////////////////////////////////////////////////////////////////////// + +func BuildCalendar[T any](things []T, dateOfThing func(T) Date) Calendar[T] { + if len(things) == 0 { + return nil + } + + newestDate := DateOf(time.Now().Local()) + + oldestDate := dateOfThing(things[0]) + byDate := make(map[Date]T, len(things)) + for _, thing := range things { + date := dateOfThing(thing) + if oldestDate.Cmp(date) > 0 { + oldestDate = date + } + byDate[date] = thing + } + + var ret Calendar[T] + for date := oldestDate; date.Cmp(newestDate) <= 0; date = date.AddDays(1) { + if len(ret) == 0 || date.Weekday() == 0 { + ret = append(ret, CalendarWeek[T]{}) + } + ret[len(ret)-1][date.Weekday()] = CalendarDay[T]{ + Date: date, + Data: byDate[date], + } + } + return ret +} diff --git a/cmd/generate/imworkingon.html.tmpl b/cmd/generate/imworkingon.html.tmpl index fb24ac6..1be3960 100644 --- a/cmd/generate/imworkingon.html.tmpl +++ b/cmd/generate/imworkingon.html.tmpl @@ -81,6 +81,78 @@ </article> {{- end }} </section> + <section id="standups"> + <h2>Daily statuses</h2> + <p>Posted daily on <a href="https://fosstodon.org/@lukeshu">Mastodon</a>.</p> + + <details><summary>Calendar view</summary> + <table> + <thead> + <tr> + <th></th> + <th><abbr title="Sunday">Su</abbr></th> + <th><abbr title="Monday">M</abbr></th> + <th><abbr title="Tuesday">Tu</abbr></th> + <th><abbr title="Wednesday">W</abbr></th> + <th><abbr title="Thursday">Th</abbr></th> + <th><abbr title="Friday">F</abbr></th> + <th><abbr title="Saturday">S</abbr></th> + <th></th> + </tr> + </thead> + <tbody> + {{- $cal := .StandupCalendar }} + {{- $curSunMonth := 0 }} + {{- $curSatMonth := 0 }} + {{- range $i, $week := reverse .StandupCalendar }} + <tr> + {{- $sun := (index $week time.Sunday) }} + {{- if not $sun.Day }} + <th></th> + {{- else if ne $sun.Month $curSunMonth }} + <th class="{{ monthClass $sun.Month }}" rowspan="{{ $cal.NumWeekdaysInMonth time.Sunday $sun.Date }}"> + <span>{{ $sun.Month }} {{ $sun.Year }}</span> + </th> + {{- $curSunMonth = $sun.Month }} + {{- end }} + {{- range $day := $week }} + {{- if not $day.Day }} + <td></td> + {{- else if not $day.Data }} + <td class="{{ monthClass $day.Month }}"> + {{ $day.Day }} + </td> + {{- else }} + <td class="{{ monthClass $day.Month }}"> + <a href="#standup-id-{{ $day.Data.ID }}"> + {{ $day.Day }} + </a> + </td> + {{- end }} + </td> + {{- end }} + {{- $sat := (index $week time.Saturday) }} + {{- if not $sat.Day }} + <th></th> + {{- else if ne $sat.Month $curSatMonth }} + <th class="{{ monthClass $sat.Month }}" rowspan="{{ $cal.NumWeekdaysInMonth time.Saturday $sat.Date }}"> + <span>{{ $sat.Month }} {{ $sat.Year }}</span> + </th> + {{- $curSatMonth = $sat.Month }} + {{- end }} + {{- end }} + </tr> + </tbody> + </table> + </details> + + {{- range $status := .Standups }} + <article class="standup" id="standup-id-{{ $status.ID }}"> + <div class="standup-title"><a href="{{ $status.URL }}">{{ timeTag $status.CreatedAt "2006-01-02" }}</a></div> + <div class="standup-content">{{ $status.Content }}</div> + </article> + {{- end }} + </section> <footer> <p>The content of this page is Copyright © Luke T. Shumaker.</p> diff --git a/cmd/generate/main.go b/cmd/generate/main.go index 249e2a5..dd226ad 100644 --- a/cmd/generate/main.go +++ b/cmd/generate/main.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "os" + "reflect" "sort" "strings" "time" @@ -40,6 +41,10 @@ var timeTagTmpl = template.Must(template.New("time.tag.tmpl"). Parse(`<time datetime="{{ .Machine }}" title="{{ .HumanVerbose }}">{{ .HumanPretty }}</time>`)) func mainWithError() error { + standups, err := ReadStandups("https://fosstodon.org", "lukeshu") + if err != nil { + return err + } contribs, err := ReadContribs("imworkingon/contribs.yml") if err != nil { return err @@ -67,6 +72,26 @@ func mainWithError() error { tmpl := template.Must(template.New("imworkingon.html.tmpl"). Funcs(template.FuncMap{ + "time": func() map[string]time.Weekday { + return map[string]time.Weekday{ + "Sunday": time.Sunday, + "Monday": time.Monday, + "Tuesday": time.Tuesday, + "Wednesday": time.Wednesday, + "Thursday": time.Thursday, + "Friday": time.Friday, + "Saturday": time.Saturday, + } + }, + "reverse": func(x any) any { + in := reflect.ValueOf(x) + l := in.Len() + out := reflect.MakeSlice(in.Type(), l, l) + for i := 0; i < l; i++ { + out.Index(l - (i + 1)).Set(in.Index(i)) + } + return out.Interface() + }, "timeTag": func(ts time.Time, prettyFmt string) (template.HTML, error) { ts = ts.Local() var out strings.Builder @@ -77,6 +102,13 @@ func mainWithError() error { }) return template.HTML(out.String()), err }, + "monthClass": func(m time.Month) string { + if m%2 == 0 { + return "even-month" + } else { + return "odd-month" + } + }, "md2html": MarkdownToHTML, "getUpstream": func(c Contribution) Upstream { // First try any of the documented upstreams. @@ -99,9 +131,11 @@ func mainWithError() error { Parse(htmlTmplStr)) var out bytes.Buffer if err := tmpl.Execute(&out, map[string]any{ - "Contribs": contribs, - "Tags": tags, - "Upstreams": upstreams, + "Contribs": contribs, + "Tags": tags, + "Upstreams": upstreams, + "Standups": standups, + "StandupCalendar": BuildCalendar(standups, func(status *MastodonStatus) Date { return DateOf(status.CreatedAt) }), }); err != nil { return err } diff --git a/cmd/generate/src_mastodon.go b/cmd/generate/src_mastodon.go new file mode 100644 index 0000000..42ae8b2 --- /dev/null +++ b/cmd/generate/src_mastodon.go @@ -0,0 +1,52 @@ +package main + +import ( + "html/template" + "net/url" + "slices" + "time" +) + +type MastodonStatus struct { + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + URL string `json:"url"` + Content template.HTML `json:"content"` +} + +// Returns statuses sorted from newest to oldest. +func ReadStandups(server, username string) ([]*MastodonStatus, error) { + var account struct { + ID string `json:"id"` + } + if err := httpGetJSON(server+"/api/v1/accounts/lookup?acct="+username, &account); err != nil { + return nil, err + } + var statuses []*MastodonStatus + for { + params := make(url.Values) + params.Set("tagged", "DailyStandUp") + params.Set("exclude_reblogs", "true") + if len(statuses) > 0 { + params.Set("max_id", statuses[len(statuses)-1].ID) + } + var resp []*MastodonStatus + if err := httpGetJSON(server+"/api/v1/accounts/"+account.ID+"/statuses?"+params.Encode(), &resp); err != nil { + return nil, err + } + if len(resp) == 0 { + break + } + statuses = append(statuses, resp...) + } + + ignoreList := []string{ + "https://fosstodon.org/@lukeshu/112198267818432116", + "https://fosstodon.org/@lukeshu/112198241414760456", + } + statuses = slices.DeleteFunc(statuses, func(status *MastodonStatus) bool { + return slices.Contains(ignoreList, status.URL) + }) + + return statuses, nil +} |