all repos — homestead @ f7d8beffaa09ecf863996d29c72f508eb3952c84

Code for my website

domain/content/posts.go (view raw)

package content

import (
	"bytes"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"alin.ovh/x/log"
	"github.com/Southclaws/fault"
	"github.com/Southclaws/fault/fmsg"
	"github.com/adrg/frontmatter"
	mapset "github.com/deckarep/golang-set/v2"

	"alin.ovh/homestead/domain/content/markdown"
	"alin.ovh/homestead/shared/vcs"
)

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
	StaticFiles []string
}

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",
		"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) {
	config.StaticFiles = append(config.StaticFiles, StaticList...)

	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 {
	ext := strings.TrimPrefix(filepath.Ext(filename), ".")
	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():
		switch ext {
		case "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)
			}
		case "html":
			cc.StaticFiles = append(cc.StaticFiles, filename)
		default:
			if slices.Contains(cc.config.StaticFiles, 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
}