Files
capytal.cc/handlers/pages/blog.templ

321 lines
6.6 KiB
Plaintext

package pages
import (
"bytes"
"io"
"encoding/json"
"strings"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"log"
"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
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)
}
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,
}
}
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) {
@layouts.Page() {
<div class="w-100% py-10rem flex justify-center">
<main class="w-60%">
@templ.Raw(string(html))
</main>
</div>
}
}
templ templateList(entries map[string]entry) {
@layouts.Page() {
<div class="w-100% py-10rem flex justify-center">
<main class="w-60%">
<h1>Blog</h1>
<ul class="list-none">
for _, e := range entries {
<li>
<h2>
<a href={ templ.SafeURL(path.Join(".", e.path)) }>
@templ.Raw(e.title)
</a>
</h2>
<p>
@templ.Raw(e.summary)
</p>
</li>
}
</ul>
</main>
</div>
}
}