rewrite code a bit
All checks were successful
/ Deploy blog to deuterium (push) Successful in 1m9s

Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
Naman Sood 2024-11-23 02:53:25 -05:00
parent 18455362d3
commit 1981a291aa
17 changed files with 916 additions and 849 deletions

213
postmap/postmap.go Normal file
View file

@ -0,0 +1,213 @@
package postmap
import (
"bytes"
"fmt"
"io"
"iter"
"log"
"os"
"prose/common"
"prose/watcher"
"slices"
"strings"
"sync"
"github.com/aymerick/raymond"
"github.com/mitchellh/mapstructure"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
highlighting "github.com/yuin/goldmark-highlighting"
meta "github.com/yuin/goldmark-meta"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
// Metadata stores the data about a post that needs to be visible
// at the home page.
type Metadata struct {
Title string
Summary string
Time int64 // unix timestamp
}
// Post stores the contents of a blog post.
type Post struct {
Slug string
Metadata Metadata
Contents string
Image []byte
}
func (p *Post) Render(tpl *raymond.Template) (string, error) {
return tpl.Exec(p)
}
func (p *Post) String() string {
return p.Slug
}
func postLess(a, b *Post) int {
return int(b.Metadata.Time - a.Metadata.Time)
}
// postList stores Posts in reverse order of Post.Metadata.Time.
type postList struct {
mu sync.RWMutex
posts []*Post
md goldmark.Markdown
}
// New returns a new postList.
func New() (watcher.OrderedAutoMap[string, *Post], error) {
files, err := os.ReadDir("posts/")
if err != nil {
return nil, err
}
pl := &postList{
posts: make([]*Post, 0, len(files)),
md: goldmark.New(
goldmark.WithExtensions(
extension.Linkify,
extension.Strikethrough,
extension.Typographer,
extension.Footnote,
meta.Meta,
highlighting.Highlighting,
emoji.New(emoji.WithRenderingMethod(emoji.Unicode)),
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
),
}
for _, f := range files {
filename := f.Name()
if filename == ".gitignore" {
continue
}
err := pl.fetchLocked(filename)
if err != nil {
log.Printf("could not render %q, skipping: %v", filename, err)
} else {
log.Printf("loaded post: %q", filename)
}
}
go watcher.Watch("posts/", pl)
return pl, nil
}
func (pl *postList) newPost(filename string) (*Post, error) {
slug, ok := strings.CutSuffix(filename, ".md")
if !ok {
return nil, fmt.Errorf("unknown file extension in posts directory: %q", filename)
}
data, err := os.ReadFile("posts/" + filename)
if err != nil {
return nil, fmt.Errorf("could not read file: %w", err)
}
var converted bytes.Buffer
ctx := parser.NewContext()
err = pl.md.Convert(data, &converted, parser.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("could not parse markdown: %w", err)
}
mdMap, err := meta.TryGet(ctx)
if err != nil {
return nil, fmt.Errorf("could not parse metadata: %w", err)
}
var metadata Metadata
err = mapstructure.Decode(mdMap, &metadata)
if err != nil {
return nil, fmt.Errorf("could not destructure metadata: %w", err)
}
post := &Post{
Slug: slug,
Metadata: metadata,
Contents: converted.String(),
}
url := common.BlogURL + "/" + slug
var buf bytes.Buffer
err = common.CreateImage(post.Metadata.Title, post.Metadata.Summary, url, &buf)
if err != nil {
return nil, fmt.Errorf("could not create post image: %w", err)
}
post.Image, err = io.ReadAll(&buf)
if err != nil {
return nil, err
}
return post, nil
}
func (pl *postList) Get(slug string) (*Post, bool) {
pl.mu.RLock()
defer pl.mu.RUnlock()
for _, p := range pl.posts {
if p.Slug == slug {
return p, true
}
}
return nil, false
}
func (pl *postList) Fetch(filename string) error {
pl.mu.Lock()
defer pl.mu.Unlock()
return pl.fetchLocked(filename)
}
func (pl *postList) fetchLocked(filename string) error {
p, err := pl.newPost(filename)
if err != nil {
return err
}
defer slices.SortFunc(pl.posts, postLess)
for i, post := range pl.posts {
if post.Slug == p.Slug {
pl.posts[i] = p
return nil
}
}
pl.posts = append(pl.posts, p)
return nil
}
func (pl *postList) Delete(filename string) error {
pl.mu.Lock()
defer pl.mu.Unlock()
slug, ok := strings.CutSuffix(filename, ".md")
if !ok {
return fmt.Errorf("unknown file extension in posts directory: %q", filename)
}
for i, post := range pl.posts {
if post.Slug == slug {
pl.posts = append(pl.posts[:i], pl.posts[i+1:]...)
break
}
}
return nil
}
// All implements watcher.OrderedCompMap[string, *Post], providing Posts in
// order of most recent first.
func (pl *postList) All() iter.Seq2[string, *Post] {
return func(yield func(string, *Post) bool) {
pl.mu.RLock()
defer pl.mu.RUnlock()
for _, p := range pl.posts {
if !yield(p.Slug, p) {
break
}
}
}
}