diff --git a/handlers/pages/blog.templ b/handlers/pages/blog.templ index ced0f63..4b654de 100644 --- a/handlers/pages/blog.templ +++ b/handlers/pages/blog.templ @@ -1,344 +1,27 @@ package pages import ( - "bytes" - "io" - "encoding/json" - "strings" - "errors" - "fmt" - "net/http" - "net/url" - "path" - "log" - "time" + "io/fs" "forge.capytal.company/capytal/www/templates/layouts" - - "forge.capytal.company/loreddev/x/groute/router/rerrors" - "forge.capytal.company/loreddev/x/groute/router" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark-meta" - "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/ast" ) -type EntryTemplate func([]byte) templ.Component -type EntryListTemplate func(map[string]entry) templ.Component - -type Blog struct { - repo string - owner string - endpoint string - - md goldmark.Markdown - entryTemplate EntryTemplate - entryListTemplate EntryListTemplate - - entries map[string]entry -} - -type entry struct { - title string - path string - summary string - creationDate time.Time - modifiedDate time.Time - contents templ.Component -} - -type BlogOptions struct { - EntryTemplate EntryTemplate -} - -func NewBlog(owner, repo, endpoint string, opts ...BlogOptions) (*Blog, error) { - /* - opt := BlogOptions{} - if len(opts) > 0 { - opt = opts[0] - } - */ - - u, err := url.Parse(endpoint) - if err != nil { - panic(fmt.Sprintf("Blog Forgejo endpoint is not a valid URL: %v", err)) - } - - md := goldmark.New( - goldmark.WithExtensions(extension.GFM, meta.Meta), - goldmark.WithParserOptions(parser.WithAutoHeadingID()), - ) - - blog := &Blog{ - repo: repo, - owner: owner, - endpoint: u.String(), - md: md, - // entryTemplate: opt.EntryTemplate, - entryTemplate: template, - entryListTemplate: templateList, - - entries: map[string]entry{}, - } - - if err := blog.init(); err != nil { - return nil, err - } - - return blog, nil -} - -func (p *Blog) Routes() router.Router { - r := router.NewRouter() - - r.HandleFunc("/{entry...}", func(w http.ResponseWriter, r *http.Request) { - pv := r.PathValue("entry") - if pv == "" { - p.listPosts(w, r) - } else { - p.blogEntry(w, r) - } - }) - - return r -} - -func (p *Blog) init() error { - _, body, rerr := p.get(fmt.Sprintf("/repos/%s/%s/contents/daily-blogs", p.owner, p.repo)) - if rerr != nil { - return rerr - } - - log.Printf("Getting files from repository") - - var list []forgejoFile - err := json.Unmarshal(body, &list) - if err != nil { - return errors.Join(errors.New("failed to parse list of entries"), err) - } - - entries := make(map[string]entry, len(list)) - for _, e := range list { - log.Printf("Getting entry %s", e.Path) - - _, body, rerr := p.get(fmt.Sprintf("/repos/%s/%s/raw/%s", p.owner, p.repo, e.Path)) - if rerr != nil { - return rerr - } - - var buf bytes.Buffer - - ctx := parser.NewContext() - - node := p.md.Parser().Parse(text.NewReader(body), parser.WithContext(ctx)) - - var title, summary string - err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkContinue, nil - } - - if summary != "" && title != "" { - return ast.WalkStop, nil - } - - if n.Kind() == ast.KindHeading && title == "" { - parts := make([]string, 0, n.ChildCount()) - - c := n.FirstChild() - for { - var tbuf bytes.Buffer - - p.md.Renderer().Render(&tbuf, body, c) - parts = append(parts, tbuf.String()) - - if s := c.NextSibling(); s != nil { - c = s - } else { - break - } - } - - title = strings.Join(parts, "") - - log.Printf("FOUND TITLE %q", title) - - return ast.WalkSkipChildren, nil - } - - if n.Kind() == ast.KindParagraph && summary == "" { - var tbuf bytes.Buffer - p.md.Renderer().Render(&tbuf, body, n) - summary = tbuf.String() - - log.Printf("FOUND SUMMARY %q", summary) - - return ast.WalkSkipChildren, nil - } - - return ast.WalkContinue, nil - }) - if err != nil { - return err - } - - meta := meta.Get(ctx) - - if t, ok := meta["title"]; ok && title == "" { - title, ok = t.(string) - if !ok { - title = "failed to concat string" // aka Yaml "yes" as bool being fucking annoying - } - } else if title == "" { - title = fmt.Sprintf("NO TITLE %s", e.Path) - } - - if s, ok := meta["summary"]; ok && summary == "" { - summary, ok = s.(string) - if !ok { - summary = "failed to concat string" // aka Yaml "yes" as bool being fucking annoying - } - } else if summary == "" { - summary = fmt.Sprintf("NO RUMMARY %s", e.Path) - } - - var created, modified time.Time - - if c, ok := meta["created"]; ok { - s, ok := c.(string) - if ok { - s = strings.Split(s, "T")[0] - created, _ = time.Parse(time.DateOnly, s) - } - } - - if m, ok := meta["modified"]; ok { - s, ok := m.(string) - if ok { - s = strings.Split(s, "T")[0] - modified, _ = time.Parse(time.DateOnly, s) - } - } - - err = p.md.Renderer().Render(&buf, body, node) - if err != nil { - return err - } - - html, err := io.ReadAll(&buf) - if err != nil { - return err - } - - comp := p.entryTemplate(html) - - entries[e.Path] = entry{ - title: title, - path: e.Path, - summary: summary, - contents: comp, - creationDate: created, - modifiedDate: modified, - } - } - - p.entries = entries - - return nil -} - -func (p *Blog) listPosts(w http.ResponseWriter, r *http.Request) { - err := p.entryListTemplate(p.entries).Render(r.Context(), w) - if err != nil { - rerrors.InternalError(err).ServeHTTP(w, r) - } -} - -func (p *Blog) blogEntry(w http.ResponseWriter, r *http.Request) { - e, ok := p.entries[r.PathValue("entry")] - if !ok { - rerrors.NotFound().ServeHTTP(w, r) - return - } - - err := e.contents.Render(r.Context(), w) - if err != nil { - rerrors.InternalError(errors.New("failed to write response"), err).ServeHTTP(w, r) - return - } -} - -func (p *Blog) get(endpoint string) (http.Header, []byte, *rerrors.RouteError) { - u, _ := url.Parse(p.endpoint) - u.Path = path.Join(u.Path, endpoint) - - r, err := http.Get(u.String()) - if err != nil { - e := rerrors.InternalError( - fmt.Errorf("failed to make request to endpoint %s", u.String()), - err, - ) - return nil, nil, &e - } - - body, err := io.ReadAll(r.Body) - if err != nil { - e := rerrors.InternalError( - fmt.Errorf("failed to read response body of request to endpoint %s", u.String()), - err, - ) - return nil, nil, &e - } else if r.StatusCode != http.StatusOK { - e := rerrors.InternalError( - fmt.Errorf("request to endpoint %s returned non-200 code %q.\n%s", u.String(), r.Status, string(body)), - ) - return nil, nil, &e - } - - return r.Header, body, nil -} - -type forgejoFile struct { - Name string `json:"name"` - Path string `json:"path"` - Sha string `json:"sha"` - LastCommitSha string `json:"last_commit_sha"` - Type string `json:"type"` -} - -templ template(html []byte) { +templ blogEntry(content string) { @layouts.Page() { -
- @templ.Raw(e.summary) -
-Created { e.creationDate.Format(time.DateOnly) } • Modified { e.modifiedDate.Format(time.DateOnly) }
-