package main import ( "bytes" "fmt" "html/template" "io" "math" "path" "sort" "strings" "time" "code.laria.me/laria.me/menu" ) type ViewMenuItem struct { Active bool Url string Title string } type ViewMenu [][]ViewMenuItem func buildViewMenuLevel( item *menu.MenuItem, current string, withNextLevel bool, ) (level []ViewMenuItem, nextLevel []ViewMenuItem) { level = make([]ViewMenuItem, 0, len(item.Children)) for _, child := range item.Children { isCur := child.Ident == current level = append(level, ViewMenuItem{ Active: isCur, Url: child.Url, Title: child.Title, }) if isCur && withNextLevel { nextLevel, _ = buildViewMenuLevel(child, "", false) } } return } func buildViewMenuLevels(item *menu.MenuItem, current string, withNextLevel bool) ViewMenu { if item == nil { return nil } viewMenu := buildViewMenuLevels(item.Parent, item.Ident, false) level, nextLevel := buildViewMenuLevel(item, current, withNextLevel) if len(level) > 0 { viewMenu = append(viewMenu, level) } if len(nextLevel) > 0 { viewMenu = append(viewMenu, nextLevel) } return viewMenu } func BuildViewMenu(menu *menu.Menu, current string) ViewMenu { curMenu := menu.Root() curMenuItem := menu.ByIdent(current) if curMenuItem != nil { curMenu = curMenuItem.Parent } return buildViewMenuLevels(curMenu, current, true) } type RootData struct { Menu ViewMenu Title string Main interface{} } type ViewArticle struct { Published time.Time Slug string Title string Content template.HTML ReadMore bool Tags []string } type Views struct { archiveDay *template.Template archive *template.Template archiveMonth *template.Template archiveYear *template.Template article *template.Template blog *template.Template content *template.Template search *template.Template start *template.Template tag *template.Template tags *template.Template } func monthText(m int) string { switch m { case 1: return "January" case 2: return "February" case 3: return "March" case 4: return "April" case 5: return "May" case 6: return "June" case 7: return "July" case 8: return "August" case 9: return "September" case 10: return "October" case 11: return "November" case 12: return "December" default: return fmt.Sprintf("", m) } } func nth(n int) string { switch n { case 1: return "1st" case 2: return "2nd" case 3: return "3rd" default: return fmt.Sprintf("%dth", n) } } type paginationArg struct { K, V string } type paginationTemplateData struct { Action string Args []paginationArg Cur int Pages int } var paginationTemplate = template.Must(template.New("").Funcs(template.FuncMap{"seq": func(max int) <-chan int { ch := make(chan int) go func() { defer close(ch) for i := 1; i <= max; i++ { ch <- i } }() return ch }}).Parse(`
{{- range .Args -}} {{- end -}} {{- $cur := .Cur -}}
`)) func normalizeDate(y, m, d int) (int, int, int) { t := time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC) y, month, d := t.Date() return y, int(month), d } func dayText(y, m, d int) string { return fmt.Sprintf("%s %s %d", nth(d), monthText(m), y) } func LoadViews(templatesDir string) (Views, error) { views := Views{} root, err := template.New("root.html").ParseFiles(path.Join(templatesDir, "root.html")) if err != nil { return views, err } root.Funcs(template.FuncMap{ "add": func(nums ...int) int { sum := 0 for _, i := range nums { sum = sum + i } return sum }, "concat": func(ss ...string) string { sb := new(strings.Builder) for _, s := range ss { sb.WriteString(s) } return sb.String() }, "nth": nth, "day_text": dayText, "month_text": monthText, "pagination": func(pages, page int, path string, queryArgs ...string) (template.HTML, error) { if len(queryArgs)%2 != 0 { return "", fmt.Errorf("pagination: need even number of query args") } args := make([]paginationArg, 0, len(queryArgs)/2) for i := 0; i < len(queryArgs); i += 2 { args = append(args, paginationArg{K: queryArgs[i], V: queryArgs[i+1]}) } buf := new(bytes.Buffer) err := paginationTemplate.Execute(buf, paginationTemplateData{ Action: path, Args: args, Cur: page, Pages: pages, }) if err != nil { return "", err } return template.HTML(buf.String()), nil }, "archive_link": func(components ...int) (string, error) { switch len(components) { case 0: return "/blog/archive", nil case 1: y := components[0] return fmt.Sprintf("/blog/%d", y), nil case 2: y := components[0] m := components[1] y, m, _ = normalizeDate(y, m, 1) return fmt.Sprintf("/blog/%d/%d", y, m), nil case 3: y := components[0] m := components[1] d := components[2] y, m, d = normalizeDate(y, m, d) return fmt.Sprintf("/blog/%d/%d/%d", y, m, d), nil default: return "", fmt.Errorf("archive_link accepts at most 3 arguments") } }, }) for name, t := range map[string]**template.Template{ "archive-day": &(views.archiveDay), "archive": &(views.archive), "archive-month": &(views.archiveMonth), "archive-year": &(views.archiveYear), "article": &(views.article), "blog": &(views.blog), "content": &(views.content), "search": &(views.search), "start": &(views.start), "tag": &(views.tag), "tags": &(views.tags), } { templateFile := path.Join(templatesDir, name+".html") if *t, err = template.Must(root.Clone()).ParseFiles(templateFile); err != nil { return views, fmt.Errorf("Failed loading template %s: %w", name, err) } } return views, nil } func (v Views) RenderArchiveDay( w io.Writer, menu *menu.Menu, curMenu string, y, m, d int, articles []ViewArticle, ) error { return v.archiveDay.Execute(w, RootData{BuildViewMenu(menu, curMenu), dayText(y, m, d), struct { Year, Month, Day int MonthText string Articles []ViewArticle }{ Year: y, Month: m, Day: d, MonthText: monthText(m), Articles: articles, }}) } type archiveEntryWithCount struct { Num int Count int } type archiveEntriesWithCount []archiveEntryWithCount func (a archiveEntriesWithCount) Len() int { return len(a) } func (a archiveEntriesWithCount) Less(i, j int) bool { return a[i].Num < a[j].Num } func (a archiveEntriesWithCount) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func buildArchiveEntries(keyedCounts map[int]int) archiveEntriesWithCount { entries := make(archiveEntriesWithCount, 0, len(keyedCounts)) for k, v := range keyedCounts { entries = append(entries, archiveEntryWithCount{ Num: k, Count: v, }) } sort.Sort(entries) return entries } func (v Views) RenderArchive( w io.Writer, menu *menu.Menu, curMenu string, countByYear map[int]int, ) error { return v.archive.Execute(w, RootData{BuildViewMenu(menu, curMenu), "Archive", struct { Years archiveEntriesWithCount }{Years: buildArchiveEntries(countByYear)}}) } func (v Views) RenderArchiveMonth( w io.Writer, menu *menu.Menu, curMenu string, year, month int, countByDay map[int]int, ) error { title := fmt.Sprintf("%s %d", monthText(month), year) return v.archiveMonth.Execute(w, RootData{BuildViewMenu(menu, curMenu), title, struct { Year, Month int Days archiveEntriesWithCount }{ Year: year, Month: month, Days: buildArchiveEntries(countByDay), }}) } func (v Views) RenderArchiveYear( w io.Writer, menu *menu.Menu, curMenu string, year int, countByMonth map[int]int, ) error { return v.archiveYear.Execute(w, RootData{BuildViewMenu(menu, curMenu), string(year), struct { Year int Months archiveEntriesWithCount }{ Year: year, Months: buildArchiveEntries(countByMonth), }}) } func (v Views) RenderArticle( w io.Writer, menu *menu.Menu, curMenu string, article ViewArticle, ) error { return v.article.Execute(w, RootData{BuildViewMenu(menu, curMenu), article.Title, article}) } func (v Views) RenderContent( w io.Writer, menu *menu.Menu, curMenu string, html template.HTML, ) error { return v.content.Execute(w, RootData{BuildViewMenu(menu, curMenu), "", html}) } func (v Views) RenderSearch( w io.Writer, menu *menu.Menu, curMenu string, query string, total int, results []ViewArticle, pages int, page int, ) error { return v.search.Execute(w, RootData{BuildViewMenu(menu, curMenu), "Search", struct { Q string Total int Results []ViewArticle Pages, Page int }{ Q: query, Total: total, Results: results, Pages: pages, Page: page, }}) } func (v Views) RenderStart( w io.Writer, menu *menu.Menu, curMenu string, content template.HTML, blogArticles []ViewArticle, ) error { return v.start.Execute(w, RootData{BuildViewMenu(menu, curMenu), "", struct { Content template.HTML Blog []ViewArticle }{ Content: content, Blog: blogArticles, }}) } func (v Views) RenderTag( w io.Writer, menu *menu.Menu, curMenu string, tag string, articles []ViewArticle, pages, page int, ) error { return v.tag.Execute(w, RootData{BuildViewMenu(menu, curMenu), "Tag " + tag, struct { Tag string Articles []ViewArticle Pages, Page int }{ Tag: tag, Articles: articles, Pages: pages, Page: page, }}) } func (v Views) RenderBlog( w io.Writer, menu *menu.Menu, curMenu string, articles []ViewArticle, pages, page int, ) error { return v.blog.Execute(w, RootData{BuildViewMenu(menu, curMenu), "Blog", struct { Articles []ViewArticle Pages, Page int }{ Articles: articles, Pages: pages, Page: page, }}) } type tagcloudTag struct { Tag string SizeClass int } type tagcloudTags []tagcloudTag func (t tagcloudTags) Len() int { return len(t) } func (t tagcloudTags) Less(i, j int) bool { return strings.ToLower(t[i].Tag) < strings.ToLower(t[j].Tag) } func (t tagcloudTags) Swap(i, j int) { t[i], t[j] = t[j], t[i] } const tagcloudCategories = 5 func (v Views) RenderTags(w io.Writer, menu *menu.Menu, curMenu string, tagCounts map[string]int) error { tags := make(tagcloudTags, 0, len(tagCounts)) maxCount := 0 for tag, count := range tagCounts { tags = append(tags, tagcloudTag{ Tag: tag, SizeClass: count, }) if count > maxCount { maxCount = count } } for i, tag := range tags { tags[i].SizeClass = int(math.Ceil((float64(tag.SizeClass) / float64(maxCount)) * tagcloudCategories)) } sort.Sort(tags) return v.tags.Execute(w, RootData{BuildViewMenu(menu, curMenu), "Tags", struct { Tags tagcloudTags }{ Tags: tags, }}) }