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" ) type Builder struct { site models.Site gmMarkdown goldmark.Markdown opts Options tmpls *template.Template } func New(site models.Site, opts Options) (*Builder, error) { 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.WithHardWraps(), 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 { return postCopy[i].Meta.Date.After(postCopy[j].Meta.Date) }) pl := postListData{} 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) { postPath := post.Meta.Slug if b.opts.BasePosts != "" { postPath = filepath.Join(b.opts.BasePosts, strings.TrimPrefix(postPath, "/")) } var md bytes.Buffer if err := b.gmMarkdown.Convert([]byte(post.Content), &md); err != nil { return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Meta.Slug, err) } return postSingleData{ Path: postPath, Meta: post.Meta, HTML: template.HTML(md.String()), }, 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{ Body: template.HTML(buf.String()), }) } type buildContext struct { outDir string }