weiro/providers/sitebuilder/builder.go

164 lines
3.6 KiB
Go
Raw Normal View History

2026-02-18 11:07:18 +00:00
package sitebuilder
import (
"bytes"
"fmt"
"html/template"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"lmika.dev/lmika/weiro/models"
2026-02-19 10:21:27 +00:00
"lmika.dev/lmika/weiro/models/pubmodel"
2026-02-18 11:07:18 +00:00
)
type Builder struct {
2026-02-19 10:21:27 +00:00
site pubmodel.Site
2026-02-18 11:07:18 +00:00
gmMarkdown goldmark.Markdown
opts Options
tmpls *template.Template
}
2026-02-19 10:21:27 +00:00
func New(site pubmodel.Site, opts Options) (*Builder, error) {
2026-02-18 11:07:18 +00:00
tmpls, err := template.New("").
Funcs(templateFns(site, opts)).
ParseFS(opts.TemplatesFS, tmplNamePostSingle, tmplNamePostList, tmplNameLayoutMain)
if err != nil {
return nil, err
}
return &Builder{
site: site,
opts: opts,
tmpls: tmpls,
gmMarkdown: goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
),
}, nil
}
func (b *Builder) BuildSite(outDir string) error {
buildCtx := buildContext{outDir: outDir}
if err := os.RemoveAll(outDir); err != nil {
return err
}
for _, post := range b.site.Posts {
if err := b.writePost(buildCtx, post); err != nil {
return err
}
}
if err := b.renderPostList(buildCtx, b.site.Posts); err != nil {
return err
}
return nil
}
func (b *Builder) renderPostList(ctx buildContext, postList []*models.Post) error {
// TODO: paging
postCopy := make([]*models.Post, len(postList))
copy(postCopy, postList)
sort.Slice(postCopy, func(i, j int) bool {
2026-02-19 10:21:27 +00:00
return postCopy[i].PublishedAt.After(postCopy[j].PublishedAt)
2026-02-18 11:07:18 +00:00
})
pl := postListData{
2026-02-19 10:21:27 +00:00
commonData: commonData{Site: b.site},
}
2026-02-18 11:07:18 +00:00
for _, post := range postCopy {
rp, err := b.renderPost(post)
if err != nil {
return err
}
pl.Posts = append(pl.Posts, rp)
}
return b.createAtPath(ctx, "", func(f io.Writer) error {
return b.renderTemplate(f, tmplNamePostList, pl)
})
}
func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
2026-02-19 10:21:27 +00:00
postPath := post.Slug
2026-02-18 11:07:18 +00:00
if b.opts.BasePosts != "" {
postPath = filepath.Join(b.opts.BasePosts, strings.TrimPrefix(postPath, "/"))
}
var md bytes.Buffer
2026-02-19 10:21:27 +00:00
if err := b.gmMarkdown.Convert([]byte(post.Body), &md); err != nil {
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Slug, err)
2026-02-18 11:07:18 +00:00
}
return postSingleData{
2026-02-19 10:21:27 +00:00
commonData: commonData{Site: b.site},
Path: postPath,
2026-02-19 10:21:27 +00:00
Meta: post,
HTML: template.HTML(md.String()),
2026-02-18 11:07:18 +00:00
}, nil
}
func (b *Builder) writePost(ctx buildContext, post *models.Post) error {
rp, err := b.renderPost(post)
if err != nil {
return err
}
return b.createAtPath(ctx, rp.Path, func(f io.Writer) error {
return b.renderTemplate(f, tmplNamePostSingle, rp)
})
}
func (b *Builder) createAtPath(ctx buildContext, path string, fn func(f io.Writer) error) error {
outFile := filepath.Join(ctx.outDir, strings.TrimPrefix(path, "/"))
if filepath.Ext(outFile) == "" {
outFile = filepath.Join(outFile, "index.html")
}
log.Printf("Writing %s\n", outFile)
// Render it within the template
if err := os.MkdirAll(filepath.Dir(outFile), 0755); err != nil {
return err
}
f, err := os.Create(outFile)
if err != nil {
return err
}
defer f.Close()
return fn(f)
}
func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) error {
var buf bytes.Buffer
if err := b.tmpls.ExecuteTemplate(&buf, name, data); err != nil {
return err
}
return b.tmpls.ExecuteTemplate(w, tmplNameLayoutMain, layoutData{
2026-02-19 10:21:27 +00:00
commonData: commonData{Site: b.site},
Body: template.HTML(buf.String()),
2026-02-18 11:07:18 +00:00
})
}
type buildContext struct {
outDir string
}