about summary refs log tree commit diff stats
path: root/internal/content
diff options
context:
space:
mode:
Diffstat (limited to 'internal/content')
-rw-r--r--internal/content/posts.go136
1 files changed, 136 insertions, 0 deletions
diff --git a/internal/content/posts.go b/internal/content/posts.go
new file mode 100644
index 0000000..f4c6c76
--- /dev/null
+++ b/internal/content/posts.go
@@ -0,0 +1,136 @@
+package content
+
+import (
+	"bytes"
+	"os"
+	"path"
+	"path/filepath"
+	"slices"
+	"strings"
+	"time"
+
+	"go.alanpearce.eu/x/log"
+
+	"github.com/adrg/frontmatter"
+	mapset "github.com/deckarep/golang-set/v2"
+	fences "github.com/stefanfritsch/goldmark-fences"
+	"github.com/yuin/goldmark"
+	"github.com/yuin/goldmark/extension"
+	htmlrenderer "github.com/yuin/goldmark/renderer/html"
+	"gitlab.com/tozd/go/errors"
+)
+
+type PostMatter struct {
+	Date        time.Time `toml:"date"`
+	Description string    `toml:"description"`
+	Title       string    `toml:"title"`
+	Taxonomies  struct {
+		Tags []string `toml:"tags"`
+	} `toml:"taxonomies"`
+}
+
+type Post struct {
+	Input    string
+	Output   string
+	Basename string
+	URL      string
+	Content  string
+	PostMatter
+}
+
+type Tags mapset.Set[string]
+
+var markdown = goldmark.New(
+	goldmark.WithRendererOptions(
+		htmlrenderer.WithUnsafe(),
+	),
+	goldmark.WithExtensions(
+		extension.GFM,
+		extension.Footnote,
+		extension.Typographer,
+		&fences.Extender{},
+	),
+)
+
+func GetPost(filename string) (*PostMatter, []byte, error) {
+	matter := PostMatter{}
+	content, err := os.Open(filename)
+	if err != nil {
+		return nil, nil, errors.WithMessagef(err, "could not open post %s", filename)
+	}
+	defer content.Close()
+	rest, err := frontmatter.MustParse(content, &matter)
+	if err != nil {
+		return nil, nil, errors.WithMessagef(
+			err,
+			"could not parse front matter of post %s",
+			filename,
+		)
+	}
+
+	return &matter, rest, nil
+}
+
+func RenderMarkdown(content []byte) (string, error) {
+	var buf bytes.Buffer
+	if err := markdown.Convert(content, &buf); err != nil {
+		return "", errors.WithMessage(err, "could not convert markdown content")
+	}
+
+	return buf.String(), nil
+}
+
+type Config struct {
+	Root      string
+	InputDir  string
+	OutputDir string
+}
+
+func ReadPosts(config *Config, log *log.Logger) ([]Post, Tags, error) {
+	tags := mapset.NewSet[string]()
+	posts := []Post{}
+	subdir := filepath.Join(config.Root, config.InputDir)
+	files, err := os.ReadDir(subdir)
+	if err != nil {
+		return nil, nil, errors.WithMessagef(err, "could not read post directory %s", subdir)
+	}
+	outputReplacer := strings.NewReplacer(config.Root, config.OutputDir, ".md", "/index.html")
+	urlReplacer := strings.NewReplacer(config.Root, "", ".md", "/")
+	for _, f := range files {
+		pathFromRoot := filepath.Join(subdir, f.Name())
+		if !f.IsDir() && path.Ext(pathFromRoot) == ".md" {
+			output := outputReplacer.Replace(pathFromRoot)
+			url := urlReplacer.Replace(pathFromRoot)
+			log.Debug("reading post", "post", pathFromRoot)
+			matter, content, err := GetPost(pathFromRoot)
+			if err != nil {
+				return nil, nil, err
+			}
+
+			for _, tag := range matter.Taxonomies.Tags {
+				tags.Add(strings.ToLower(tag))
+			}
+
+			log.Debug("rendering markdown in post", "post", pathFromRoot)
+			html, err := RenderMarkdown(content)
+			if err != nil {
+				return nil, nil, err
+			}
+			post := Post{
+				Input:      pathFromRoot,
+				Output:     output,
+				Basename:   filepath.Base(url),
+				URL:        url,
+				PostMatter: *matter,
+				Content:    html,
+			}
+
+			posts = append(posts, post)
+		}
+	}
+	slices.SortFunc(posts, func(a, b Post) int {
+		return b.Date.Compare(a.Date)
+	})
+
+	return posts, tags, nil
+}