package content
import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"strings"
"time"
"alin.ovh/homestead/domain/content/markdown"
"alin.ovh/homestead/shared/vcs"
"alin.ovh/x/log"
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fmsg"
"github.com/adrg/frontmatter"
mapset "github.com/deckarep/golang-set/v2"
)
type PostMatter struct {
Date time.Time
Description string
Title string
Taxonomies struct {
Tags []string
}
}
type Post struct {
Input string
Basename string
URL string
Commits []*vcs.Commit
*PostMatter
content []byte
}
type Config struct {
Root string
PostDir string
Repo *vcs.Repository
}
type Collection struct {
config *Config
log *log.Logger
Posts []*Post
Pages []*Post
StaticFiles []string
Tags mapset.Set[string]
}
var (
postURLReplacer = strings.NewReplacer(
"index.md", "",
".md", "/",
)
pageURLReplacer = strings.NewReplacer(
"index.md", "",
".md", "",
)
StaticList = []string{
"config.toml",
"cv.html",
"public_key.asc",
}
)
func parse(fp string, post *Post) error {
content, err := os.Open(fp)
if err != nil {
return fault.Wrap(err, fmsg.With(fmt.Sprintf("could not open post %s", fp)))
}
defer content.Close()
post.content, err = frontmatter.Parse(content, post.PostMatter)
if err != nil {
return fault.Wrap(err, fmsg.With(fmt.Sprintf("could not parse front matter of post %s",
fp)))
}
return nil
}
// implements gomponent.Node
func (p *Post) Render(w io.Writer) error {
return markdown.Convert(p.content, w)
}
func (p *Post) RenderString() (string, error) {
var buf bytes.Buffer
if err := p.Render(&buf); err != nil {
return "", fault.Wrap(err, fmsg.With("could not convert markdown content"))
}
return buf.String(), nil
}
func NewContentCollection(config *Config, log *log.Logger) (*Collection, error) {
cc := &Collection{
Posts: []*Post{},
Tags: mapset.NewSet[string](),
Pages: []*Post{},
StaticFiles: []string{},
config: config,
log: log,
}
err := filepath.WalkDir(config.Root, func(filename string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
filename, err = filepath.Rel(config.Root, filename)
if err != nil {
return err
}
log.Debug("walking", "filename", filename)
return cc.HandleFile(filename, d)
})
slices.SortFunc(cc.Posts, func(a, b *Post) int {
return b.Date.Compare(a.Date)
})
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not walk directory"))
}
return cc, nil
}
func (cc *Collection) HandleFile(filename string, d fs.DirEntry) error {
switch {
case strings.HasPrefix(filename, ".") &&
filename != "." &&
!strings.HasPrefix(filename, ".well-known"):
cc.log.Debug("skipping", "filename", filename, "is_dir", d.Type().IsDir())
if d.Type().IsDir() {
return fs.SkipDir
}
case !d.Type().IsDir():
if filepath.Ext(filename) == ".md" {
if strings.HasPrefix(filename, cc.config.PostDir) {
post, err := cc.GetPost(filename)
if err != nil {
return err
}
for _, tag := range post.Taxonomies.Tags {
cc.Tags.Add(strings.ToLower(tag))
}
cc.Posts = append(cc.Posts, post)
} else {
page, err := cc.GetPage(filename)
if err != nil {
return err
}
cc.Pages = append(cc.Pages, page)
}
} else if slices.Contains(StaticList, filename) {
cc.StaticFiles = append(cc.StaticFiles, filename)
}
}
return nil
}
func (cc *Collection) GetPost(filename string) (*Post, error) {
fp := filepath.Join(cc.config.Root, filename)
url := path.Join("/", postURLReplacer.Replace(filename)) + "/"
cs, err := cc.config.Repo.GetFileLog(filename)
if err != nil {
return nil, fault.Wrap(
err,
fmsg.With(fmt.Sprintf("could not get commit log for file %s", filename)),
)
}
post := &Post{
Input: filename,
Basename: filepath.Base(url),
URL: url,
PostMatter: &PostMatter{},
Commits: cs,
}
err = parse(fp, post)
if err != nil {
return nil, err
}
return post, nil
}
func (cc *Collection) GetPage(filename string) (*Post, error) {
fp := filepath.Join(cc.config.Root, filename)
url := path.Join("/", pageURLReplacer.Replace(filename))
cs, err := cc.config.Repo.GetFileLog(filename)
if err != nil {
return nil, fault.Wrap(
err,
fmsg.With(fmt.Sprintf("could not get commit log for file %s", filename)),
)
}
post := &Post{
Input: filename,
Basename: filepath.Base(url),
URL: url,
PostMatter: &PostMatter{},
Commits: cs,
}
err = parse(fp, post)
if err != nil {
return nil, err
}
if post.Date.IsZero() && len(cs) > 1 {
post.Date = cs[0].Date
}
return post, nil
}
domain/content/posts.go (view raw)