package content import ( "bytes" "context" "io" "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 Description string Title string Taxonomies struct { Tags []string } } type Post struct { Input string Output string Basename string URL string *PostMatter content []byte } type Config struct { Root string InputDir string } type PostsCollection struct { config *Config Posts []Post Tags mapset.Set[string] } var markdown = goldmark.New( goldmark.WithRendererOptions( htmlrenderer.WithUnsafe(), ), goldmark.WithExtensions( extension.GFM, extension.Footnote, extension.Typographer, &fences.Extender{}, ), ) var outputReplacer = strings.NewReplacer( "index.md", "index.html", ".md", "/index.html", ) var urlReplacer = strings.NewReplacer( "index.md", "", ".md", "/", ) func (pc *PostsCollection) GetPost(filename string) (*Post, error) { fp := filepath.Join(pc.config.Root, filename) url := path.Join("/", urlReplacer.Replace(filename)) + "/" post := &Post{ Input: fp, Output: path.Join(pc.config.InputDir, outputReplacer.Replace(filename)), Basename: filepath.Base(url), URL: url, PostMatter: &PostMatter{}, } content, err := os.Open(fp) if err != nil { return nil, errors.WithMessagef(err, "could not open post %s", fp) } defer content.Close() post.content, err = frontmatter.Parse(content, post.PostMatter) if err != nil { return nil, errors.WithMessagef( err, "could not parse front matter of post %s", fp, ) } return post, nil } // implements templ.Component func (p *Post) Render(_ context.Context, w io.Writer) error { return markdown.Convert(p.content, w) } func (p *Post) RenderString() (string, error) { var buf bytes.Buffer if err := p.Render(context.Background(), &buf); err != nil { return "", errors.WithMessage(err, "could not convert markdown content") } return buf.String(), nil } func NewPostsCollection(config *Config, log *log.Logger) (*PostsCollection, error) { pc := &PostsCollection{ Posts: []Post{}, Tags: mapset.NewSet[string](), config: config, } log.Debug("reading posts", "root", config.Root, "input_dir", config.InputDir) subdir := filepath.Join(config.Root, config.InputDir) files, err := os.ReadDir(subdir) if err != nil { return nil, errors.WithMessagef(err, "could not read post directory %s", subdir) } for _, f := range files { fn := f.Name() if !f.IsDir() && path.Ext(fn) == ".md" { log.Debug("reading post", "post", fn) post, err := pc.GetPost(filepath.Join(config.InputDir, fn)) if err != nil { return nil, err } for _, tag := range post.PostMatter.Taxonomies.Tags { pc.Tags.Add(strings.ToLower(tag)) } pc.Posts = append(pc.Posts, *post) } } slices.SortFunc(pc.Posts, func(a, b Post) int { return b.Date.Compare(a.Date) }) return pc, nil }