package website
import (
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"path"
"regexp"
"strings"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/templates"
)
func CineraIndex(c *RequestContext) ResponseData {
topic := c.PathParams["topic"]
slug := c.CurrentProject.Slug
_, foundTopic := topicsForProject(slug, topic)
if foundTopic == "" {
return FourOhFour(c)
}
indexPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, fmt.Sprintf("%s.index", foundTopic))
content, err := os.ReadFile(indexPath)
if err != nil {
return FourOhFour(c)
}
var res ResponseData
res.Write(content)
return res
}
var episodeListRegex = regexp.MustCompile(`(?ism)^.*
(?P.*).*$`)
type EpisodeListData struct {
templates.BaseData
Content template.HTML
CurrentTopic string
Topics []templates.Link
}
func EpisodeList(c *RequestContext) ResponseData {
topic := c.PathParams["topic"]
slug := c.CurrentProject.Slug
defaultTopic, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
if !hasEpisodeGuide {
return c.Redirect(c.UrlContext.BuildHomepage(), http.StatusSeeOther)
}
if topic == "" {
return c.Redirect(c.UrlContext.BuildEpisodeList(defaultTopic), http.StatusSeeOther)
}
allTopics, foundTopic := topicsForProject(slug, topic)
if foundTopic == "" {
return FourOhFour(c)
}
htmlPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, "index.html")
htmlContent, err := os.ReadFile(htmlPath)
if err != nil {
return FourOhFour(c)
}
matches := episodeListRegex.FindStringSubmatch(string(htmlContent))
if matches == nil || matches[episodeListRegex.SubexpIndex("guide")] == "" {
c.Logger.Error().Str("Filename", htmlPath).Msg("Episode guide index.html can't be parsed.")
return FourOhFour(c)
}
guide := matches[episodeListRegex.SubexpIndex("guide")]
var topicLinks []templates.Link
for _, t := range allTopics {
url := ""
if t != foundTopic {
url = c.UrlContext.BuildEpisodeList(t)
}
topicLinks = append(topicLinks, templates.Link{LinkText: t, Url: url})
}
var res ResponseData
baseData := getBaseDataAutocrumb(c, fmt.Sprintf("Episode Guide"))
res.MustWriteTemplate("episode_list.html", EpisodeListData{
BaseData: baseData,
Content: template.HTML(guide),
CurrentTopic: foundTopic,
Topics: topicLinks,
}, c.Perf)
return res
}
var episodeTitleRegex = regexp.MustCompile(`(?ism)(?P.*?)`)
var episodeContentRegex = regexp.MustCompile(`(?ism)(?P.*)`)
type EpisodeData struct {
templates.BaseData
Content template.HTML
}
func Episode(c *RequestContext) ResponseData {
episode := c.PathParams["episode"]
topic := c.PathParams["topic"]
slug := c.CurrentProject.Slug
_, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
if !hasEpisodeGuide {
return c.Redirect(c.UrlContext.BuildHomepage(), http.StatusSeeOther)
}
_, foundTopic := topicsForProject(slug, topic)
if foundTopic == "" {
return FourOhFour(c)
}
foundEpisode := findEpisode(slug, foundTopic, episode)
if foundEpisode == "" {
return FourOhFour(c)
}
htmlPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, "episode", foundTopic, foundEpisode, "index.html")
htmlContent, err := os.ReadFile(htmlPath)
if err != nil {
return FourOhFour(c)
}
titleMatches := episodeTitleRegex.FindStringSubmatch(string(htmlContent))
title := fmt.Sprintf("%s Episode Guide", c.CurrentProject.Name)
if titleMatches != nil && titleMatches[episodeTitleRegex.SubexpIndex("title")] != "" {
title = fmt.Sprintf("%s | %s", titleMatches[episodeTitleRegex.SubexpIndex("title")], title)
}
contentMatches := episodeContentRegex.FindStringSubmatch(string(htmlContent))
if contentMatches == nil || contentMatches[episodeContentRegex.SubexpIndex("content")] == "" {
c.Logger.Error().Str("filename", htmlPath).Msg("Episode file can't be parsed.")
return FourOhFour(c)
}
content := contentMatches[episodeContentRegex.SubexpIndex("content")]
var res ResponseData
baseData := getBaseData(
c,
title,
[]templates.Breadcrumb{{Name: "Episode Guide", Url: c.UrlContext.BuildEpisodeList(foundTopic)}},
)
res.MustWriteTemplate("episode.html", EpisodeData{
BaseData: baseData,
Content: template.HTML(content),
}, c.Perf)
return res
}
func topicsForProject(projectSlug string, requestedTopic string) ([]string, string) {
searchPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, projectSlug)
entries, err := fs.ReadDir(os.DirFS(searchPath), ".")
if err != nil {
return nil, ""
}
var allTopics []string
foundTopic := ""
for _, entry := range entries {
if entry.IsDir() {
t := entry.Name()
allTopics = append(allTopics, t)
if strings.ToLower(t) == strings.ToLower(requestedTopic) {
foundTopic = t
}
}
}
return allTopics, foundTopic
}
// NOTE(asaf): Assuming topic is valid. Please verify before calling.
func findEpisode(projectSlug string, topic string, requestedEpisode string) string {
searchPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, projectSlug, topic, "episode", topic) // NOTE(asaf): Yes. We have `topic` twice in the path.
entries, err := fs.ReadDir(os.DirFS(searchPath), ".")
if err != nil {
return ""
}
for _, entry := range entries {
if entry.IsDir() {
episode := entry.Name()
if strings.ToLower(episode) == strings.ToLower(requestedEpisode) {
return episode
}
}
}
return ""
}