internal/pipeline/stage/write.go

package stage

import (
	"context"
	"fmt"
	"html/template"
	"io"
	"os"
	"path/filepath"
	"strings"

	"mercemay.top/src/tilstream/internal/pipeline"
)

// Write renders each post through a Go html/template and writes the
// resulting HTML to OutputDir. The template receives a post view with
// fields Title, URL, HTML, and Meta.
type Write struct {
	Template *template.Template
	FileMode os.FileMode
}

// Name returns the stage name.
func (*Write) Name() string { return "write" }

// NewWrite returns a Write stage that will render through tmpl.
func NewWrite(tmpl *template.Template) *Write {
	return &Write{Template: tmpl, FileMode: 0o644}
}

// Run creates OutputDir if needed and writes one HTML file per post.
func (w *Write) Run(ctx context.Context, st *pipeline.State) error {
	if err := os.MkdirAll(st.OutputDir, 0o755); err != nil {
		return err
	}
	for i := range st.Posts {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		if err := w.writeOne(st.OutputDir, &st.Posts[i]); err != nil {
			return err
		}
	}
	return nil
}

func (w *Write) writeOne(dir string, p *pipeline.Post) error {
	slug := strings.TrimSuffix(filepath.Base(p.Path), ".md")
	if s := p.Meta["slug"]; s != "" {
		slug = s
	}
	out := filepath.Join(dir, slug+".html")
	f, err := os.OpenFile(out, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, w.FileMode)
	if err != nil {
		return fmt.Errorf("write: %w", err)
	}
	defer f.Close()

	view := struct {
		Title string
		URL   string
		HTML  template.HTML
		Meta  map[string]string
	}{
		Title: p.Meta["title"],
		URL:   "/" + slug + ".html",
		HTML:  template.HTML(p.HTML),
		Meta:  p.Meta,
	}
	if w.Template == nil {
		return writePlain(f, view.HTML)
	}
	return w.Template.Execute(f, view)
}

func writePlain(w io.Writer, body template.HTML) error {
	_, err := io.WriteString(w, string(body))
	return err
}

// MustLoadTemplate is a test helper that loads every .tmpl file in a dir
// into a single Template instance.
func MustLoadTemplate(dir string) *template.Template {
	return template.Must(template.ParseGlob(filepath.Join(dir, "*.tmpl")))
}