blog/post/post.go

180 lines
3.4 KiB
Go
Raw Normal View History

package post
import (
"html/template"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"go.jolheiser.com/blog/markdown"
"github.com/fsnotify/fsnotify"
"github.com/rs/zerolog/log"
"go.jolheiser.com/emdbed"
)
func NewBlog(basePath string) (*Blog, error) {
posts, err := scan(basePath)
if err != nil {
return nil, err
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
if err := watcher.Add(basePath); err != nil {
return nil, err
}
b := &Blog{
Path: basePath,
posts: posts,
}
go func() {
var debounce time.Time
for {
select {
case event := <-watcher.Events:
log.Debug().Str("op", event.Op.String()).Msgf("fsnotify %q", event.Name)
debounce = time.Now()
case err := <-watcher.Errors:
log.Err(err).Msg("")
default:
if !debounce.IsZero() && time.Since(debounce) > (time.Second*5) {
log.Info().Msg("reloading blog")
if err := b.Scan(); err != nil {
log.Err(err).Msg("")
}
debounce = time.Time{}
}
}
}
}()
return b, nil
}
func scan(basePath string) (map[string]*Post, error) {
posts := make(map[string]*Post)
ents, err := os.ReadDir(basePath)
if err != nil {
return nil, err
}
for _, ent := range ents {
var apn string
if ent.IsDir() {
apn = filepath.Join(basePath, ent.Name(), "index.md")
if _, err := os.Stat(apn); err != nil {
log.Error().Err(err).Msgf("could not parse %s", ent.Name())
continue
}
} else {
if !strings.HasSuffix(ent.Name(), ".md") {
continue
}
apn = filepath.Join(basePath, ent.Name())
}
fi, err := os.Open(apn)
if err != nil {
log.Error().Err(err).Msg("could not open file")
continue
}
post := &Post{Path: apn, Slug: strings.TrimSuffix(ent.Name(), ".md")}
if err := markdown.Meta(fi, &post); err != nil {
log.Error().Err(err).Msg("could not extract meta")
continue
}
posts[post.Slug] = post
if err := fi.Close(); err != nil {
log.Error().Err(err).Msg("could not close file")
continue
}
if err := post.Load(); err != nil {
log.Err(err).Msg("could not load post")
continue
}
}
return posts, nil
}
type Blog struct {
Path string
posts map[string]*Post
mx sync.RWMutex
}
func (b *Blog) Scan() error {
posts, err := scan(b.Path)
if err != nil {
return err
}
b.mx.Lock()
defer b.mx.Unlock()
b.posts = posts
return nil
}
func (b *Blog) SortedPosts() []*Post {
posts := make([]*Post, 0, len(b.posts))
for _, post := range b.posts {
posts = append(posts, post)
}
sort.Slice(posts, func(i, j int) bool {
return posts[i].Date.After(posts[j].Date)
})
return posts
}
func (b *Blog) Post(name string) (*Post, bool) {
b.mx.RLock()
defer b.mx.RUnlock()
if post, ok := b.posts[name]; ok {
return post, true
}
return nil, false
}
type Post struct {
Path string `toml:"-"`
Slug string `toml:"-"`
Content template.HTML `toml:"-"`
Title string `toml:"title"`
Author string `toml:"author"`
Date time.Time `toml:"date"`
Tags []string `toml:"tags"`
}
func (p *Post) Load() error {
fi, err := os.Open(p.Path)
if err != nil {
return err
}
defer fi.Close()
mdContent, err := markdown.Content(fi)
if err != nil {
return err
}
emdbedContent, err := emdbed.Convert(filepath.Dir(p.Path), strings.NewReader(mdContent))
if err != nil {
return err
}
md, err := markdown.Convert(strings.NewReader(emdbedContent))
if err != nil {
return err
}
p.Content = template.HTML(md)
return nil
}