about summary refs log tree commit diff stats
path: root/internal/builder
diff options
context:
space:
mode:
Diffstat (limited to 'internal/builder')
-rw-r--r--internal/builder/builder.go88
-rw-r--r--internal/builder/template.go110
2 files changed, 85 insertions, 113 deletions
diff --git a/internal/builder/builder.go b/internal/builder/builder.go
index 4436151..b99d919 100644
--- a/internal/builder/builder.go
+++ b/internal/builder/builder.go
@@ -6,23 +6,24 @@ import (
 	"io"
 	"os"
 	"path"
+	"path/filepath"
 	"slices"
 	"time"
 
-	"website/internal/config"
-	"website/internal/content"
-	"website/internal/log"
-	"website/internal/sitemap"
-	"website/templates"
+	"go.alanpearce.eu/website/internal/config"
+	"go.alanpearce.eu/website/internal/content"
+	"go.alanpearce.eu/x/log"
+	"go.alanpearce.eu/website/internal/sitemap"
+	"go.alanpearce.eu/website/templates"
 
 	"github.com/a-h/templ"
 	mapset "github.com/deckarep/golang-set/v2"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 type IOConfig struct {
 	Source      string `conf:"default:.,short:s,flag:src"`
-	Destination string `conf:"default:website,short:d,flag:dest"`
+	Destination string `conf:"default:public,short:d,flag:dest"`
 	Development bool   `conf:"default:false,flag:dev"`
 }
 
@@ -86,30 +87,34 @@ func writerToFile(writer io.WriterTo, pathParts ...string) error {
 	return nil
 }
 
-func build(outDir string, config config.Config) (*Result, error) {
+func joinSourcePath(src string) func(string) string {
+	return func(rel string) string {
+		return filepath.Join(src, rel)
+	}
+}
+
+func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result, error) {
+	outDir := ioConfig.Destination
+	joinSource := joinSourcePath(ioConfig.Source)
 	log.Debug("output", "dir", outDir)
 	r := &Result{
 		Hashes: make([]string, 0),
 	}
-	privateDir := path.Join(outDir, "private")
-	if err := mkdirp(privateDir); err != nil {
-		return nil, errors.WithMessage(err, "could not create private directory")
-	}
-	publicDir := path.Join(outDir, "public")
-	if err := mkdirp(publicDir); err != nil {
-		return nil, errors.WithMessage(err, "could not create public directory")
-	}
 
-	err := copyRecursive("static", publicDir)
+	err := copyRecursive(joinSource("static"), outDir)
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not copy static files")
 	}
 
-	if err := mkdirp(publicDir, "post"); err != nil {
+	if err := mkdirp(outDir, "post"); err != nil {
 		return nil, errors.WithMessage(err, "could not create post output directory")
 	}
 	log.Debug("reading posts")
-	posts, tags, err := content.ReadPosts("content", "post", publicDir)
+	posts, tags, err := content.ReadPosts(&content.Config{
+		Root:      joinSource("content"),
+		InputDir:  "post",
+		OutputDir: outDir,
+	}, log.Named("content"))
 	if err != nil {
 		return nil, err
 	}
@@ -121,7 +126,7 @@ func build(outDir string, config config.Config) (*Result, error) {
 	}
 
 	for _, post := range posts {
-		if err := mkdirp(publicDir, "post", post.Basename); err != nil {
+		if err := mkdirp(outDir, "post", post.Basename); err != nil {
 			return nil, errors.WithMessage(err, "could not create directory for post")
 		}
 		log.Debug("rendering post", "post", post.Basename)
@@ -131,13 +136,13 @@ func build(outDir string, config config.Config) (*Result, error) {
 		}
 	}
 
-	if err := mkdirp(publicDir, "tags"); err != nil {
+	if err := mkdirp(outDir, "tags"); err != nil {
 		return nil, errors.WithMessage(err, "could not create directory for tags")
 	}
 	log.Debug("rendering tags list")
 	if err := renderToFile(
 		templates.TagsPage(config, "tags", mapset.Sorted(tags), "/tags"),
-		publicDir,
+		outDir,
 		"tags",
 		"index.html",
 	); err != nil {
@@ -152,14 +157,14 @@ func build(outDir string, config config.Config) (*Result, error) {
 				matchingPosts = append(matchingPosts, post)
 			}
 		}
-		if err := mkdirp(publicDir, "tags", tag); err != nil {
+		if err := mkdirp(outDir, "tags", tag); err != nil {
 			return nil, errors.WithMessage(err, "could not create directory")
 		}
 		log.Debug("rendering tags page", "tag", tag)
 		url := "/tags/" + tag
 		if err := renderToFile(
 			templates.TagPage(config, tag, matchingPosts, url),
-			publicDir,
+			outDir,
 			"tags",
 			tag,
 			"index.html",
@@ -178,13 +183,13 @@ func build(outDir string, config config.Config) (*Result, error) {
 		if err != nil {
 			return nil, errors.WithMessage(err, "could not render tag feed page")
 		}
-		if err := outputToFile(feed, publicDir, "tags", tag, "atom.xml"); err != nil {
+		if err := writerToFile(feed, outDir, "tags", tag, "atom.xml"); err != nil {
 			return nil, err
 		}
 	}
 
 	log.Debug("rendering list page")
-	if err := renderToFile(templates.ListPage(config, posts, "/post"), publicDir, "post", "index.html"); err != nil {
+	if err := renderToFile(templates.ListPage(config, posts, "/post"), outDir, "post", "index.html"); err != nil {
 		return nil, err
 	}
 	sitemap.AddPath("/post/", lastMod)
@@ -194,16 +199,16 @@ func build(outDir string, config config.Config) (*Result, error) {
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not render feed")
 	}
-	if err := outputToFile(feed, publicDir, "atom.xml"); err != nil {
+	if err := writerToFile(feed, outDir, "atom.xml"); err != nil {
 		return nil, err
 	}
 
 	log.Debug("rendering feed styles")
-	feedStyles, err := renderFeedStyles()
+	feedStyles, err := renderFeedStyles(ioConfig.Source)
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not render feed styles")
 	}
-	if err := outputToFile(feedStyles, publicDir, "feed-styles.xsl"); err != nil {
+	if err := outputToFile(feedStyles, outDir, "feed-styles.xsl"); err != nil {
 		return nil, err
 	}
 	_, err = feedStyles.Seek(0, 0)
@@ -217,7 +222,7 @@ func build(outDir string, config config.Config) (*Result, error) {
 	r.Hashes = append(r.Hashes, h)
 
 	log.Debug("rendering homepage")
-	_, text, err := content.GetPost(path.Join("content", "index.md"))
+	_, text, err := content.GetPost(joinSource(filepath.Join("content", "index.md")))
 	if err != nil {
 		return nil, err
 	}
@@ -225,43 +230,42 @@ func build(outDir string, config config.Config) (*Result, error) {
 	if err != nil {
 		return nil, err
 	}
-	if err := renderToFile(templates.Homepage(config, posts, content), publicDir, "index.html"); err != nil {
+	if err := renderToFile(templates.Homepage(config, posts, content), outDir, "index.html"); err != nil {
 		return nil, err
 	}
 	// it would be nice to set LastMod here, but using the latest post
 	// date would be wrong as the homepage has its own content file
 	// without a date, which could be newer
 	sitemap.AddPath("/", time.Time{})
-	h, _ = getHTMLStyleHash(publicDir, "index.html")
+	h, _ = getHTMLStyleHash(outDir, "index.html")
 	r.Hashes = append(r.Hashes, h)
 
 	log.Debug("rendering sitemap")
-	if err := writerToFile(sitemap, publicDir, "sitemap.xml"); err != nil {
+	if err := writerToFile(sitemap, outDir, "sitemap.xml"); err != nil {
 		return nil, err
 	}
 
 	log.Debug("rendering robots.txt")
-	rob, err := renderRobotsTXT(config)
+	rob, err := renderRobotsTXT(ioConfig.Source, config)
 	if err != nil {
 		return nil, err
 	}
-	if err := outputToFile(rob, publicDir, "robots.txt"); err != nil {
+	if err := outputToFile(rob, outDir, "robots.txt"); err != nil {
 		return nil, err
 	}
 
 	return r, nil
 }
 
-func BuildSite(ioConfig IOConfig) (*Result, error) {
-	config, err := config.GetConfig()
-	if err != nil {
-		return nil, errors.WithMessage(err, "could not get config")
+func BuildSite(ioConfig *IOConfig, cfg *config.Config, log *log.Logger) (*Result, error) {
+	if cfg == nil {
+		return nil, errors.New("config is nil")
 	}
-	config.InjectLiveReload = ioConfig.Development
+	cfg.InjectLiveReload = ioConfig.Development
 	compressFiles = !ioConfig.Development
 
 	templates.Setup()
-	loadCSS()
+	loadCSS(ioConfig.Source)
 
-	return build(ioConfig.Destination, *config)
+	return build(ioConfig, cfg, log)
 }
diff --git a/internal/builder/template.go b/internal/builder/template.go
index da45fb7..9f019df 100644
--- a/internal/builder/template.go
+++ b/internal/builder/template.go
@@ -1,53 +1,41 @@
 package builder
 
 import (
+	"bytes"
 	"encoding/xml"
 	"io"
 	"os"
 	"path/filepath"
 	"strings"
 	"text/template"
-	"website/internal/atom"
-	"website/internal/config"
-	"website/internal/content"
+
+	"go.alanpearce.eu/website/internal/atom"
+	"go.alanpearce.eu/website/internal/config"
+	"go.alanpearce.eu/website/internal/content"
 
 	"github.com/PuerkitoBio/goquery"
 	"github.com/antchfx/xmlquery"
 	"github.com/antchfx/xpath"
-	"github.com/pkg/errors"
+	"gitlab.com/tozd/go/errors"
 )
 
 var (
-	css           string
-	templateFiles = make(map[string]*os.File)
-	nsMap         = map[string]string{
+	css   string
+	nsMap = map[string]string{
 		"xsl":   "http://www.w3.org/1999/XSL/Transform",
 		"atom":  "http://www.w3.org/2005/Atom",
 		"xhtml": "http://www.w3.org/1999/xhtml",
 	}
 )
 
-func loadCSS() {
-	bytes, err := os.ReadFile("templates/style.css")
+func loadCSS(source string) {
+	bytes, err := os.ReadFile(filepath.Join(source, "templates/style.css"))
 	if err != nil {
 		panic(err)
 	}
 	css = string(bytes)
 }
 
-func loadTemplate(path string) (file *os.File, err error) {
-	if templateFiles[path] == nil {
-		file, err = os.OpenFile(path, os.O_RDONLY, 0)
-		if err != nil {
-			return nil, errors.Wrapf(err, "could not load template at path %s", path)
-		}
-		templateFiles[path] = file
-	}
-	file = templateFiles[path]
-
-	return
-}
-
 type QuerySelection struct {
 	*goquery.Selection
 }
@@ -66,9 +54,9 @@ func (q *QueryDocument) Find(selector string) *QuerySelection {
 	return &QuerySelection{q.Document.Find(selector)}
 }
 
-func renderRobotsTXT(config config.Config) (io.Reader, error) {
+func renderRobotsTXT(source string, config *config.Config) (io.Reader, error) {
 	r, w := io.Pipe()
-	tpl, err := template.ParseFiles("templates/robots.tmpl")
+	tpl, err := template.ParseFiles(filepath.Join(source, "templates/robots.tmpl"))
 	if err != nil {
 		return nil, err
 	}
@@ -87,41 +75,30 @@ func renderRobotsTXT(config config.Config) (io.Reader, error) {
 
 func renderFeed(
 	title string,
-	config config.Config,
+	config *config.Config,
 	posts []content.Post,
 	specific string,
-) (io.Reader, error) {
-	reader, err := loadTemplate("templates/feed.xml")
+) (io.WriterTo, error) {
+	buf := &bytes.Buffer{}
+	datetime := posts[0].Date.UTC()
+
+	buf.WriteString(xml.Header)
+	err := atom.LinkXSL(buf, "/feed-styles.xsl")
 	if err != nil {
 		return nil, err
 	}
-	defer func() {
-		_, err := reader.Seek(0, io.SeekStart)
-		if err != nil {
-			panic("could not reset reader: " + err.Error())
-		}
-	}()
-	doc, err := xmlquery.Parse(reader)
-	if err != nil {
-		return nil, errors.Wrap(err, "could not parse XML")
-	}
-	feed := doc.SelectElement("feed")
-	feed.SelectElement("title").FirstChild.Data = title
-	feed.SelectElement("link").SetAttr("href", config.BaseURL.String())
-	feed.SelectElement("id").FirstChild.Data = atom.MakeTagURI(config, specific)
-	datetime, err := posts[0].Date.UTC().MarshalText()
-	if err != nil {
-		return nil, errors.Wrap(err, "could not convert post date to text")
+	feed := &atom.Feed{
+		Title:   title,
+		Link:    atom.MakeLink(config.BaseURL.URL),
+		ID:      atom.MakeTagURI(config, specific),
+		Updated: datetime,
+		Entries: make([]*atom.FeedEntry, len(posts)),
 	}
-	feed.SelectElement("updated").FirstChild.Data = string(datetime)
-	tpl := feed.SelectElement("entry")
-	xmlquery.RemoveFromTree(tpl)
 
-	for _, post := range posts {
-		fullURL := config.BaseURL.JoinPath(post.URL).String()
-		text, err := xml.MarshalIndent(&atom.FeedEntry{
+	for i, post := range posts {
+		feed.Entries[i] = &atom.FeedEntry{
 			Title:   post.Title,
-			Link:    atom.MakeLink(fullURL),
+			Link:    atom.MakeLink(config.BaseURL.JoinPath(post.URL)),
 			ID:      atom.MakeTagURI(config, post.Basename),
 			Updated: post.Date.UTC(),
 			Summary: post.Description,
@@ -130,31 +107,19 @@ func renderFeed(
 				Content: post.Content,
 				Type:    "html",
 			},
-		}, "  ", "    ")
-		if err != nil {
-			return nil, errors.Wrap(err, "could not marshal xml")
-		}
-		entry, err := xmlquery.ParseWithOptions(
-			strings.NewReader(string(text)),
-			xmlquery.ParserOptions{
-				Decoder: &xmlquery.DecoderOptions{
-					Strict:    false,
-					AutoClose: xml.HTMLAutoClose,
-					Entity:    xml.HTMLEntity,
-				},
-			},
-		)
-		if err != nil {
-			return nil, errors.Wrap(err, "could not parse XML")
 		}
-		xmlquery.AddChild(feed, entry.SelectElement("entry"))
+	}
+	enc := xml.NewEncoder(buf)
+	err = enc.Encode(feed)
+	if err != nil {
+		return nil, err
 	}
 
-	return strings.NewReader(doc.OutputXML(true)), nil
+	return buf, nil
 }
 
-func renderFeedStyles() (*strings.Reader, error) {
-	tpl, err := template.ParseFiles("templates/feed-styles.xsl")
+func renderFeedStyles(source string) (*strings.Reader, error) {
+	tpl, err := template.ParseFiles(filepath.Join(source, "templates/feed-styles.xsl"))
 	if err != nil {
 		return nil, err
 	}
@@ -169,6 +134,9 @@ func renderFeedStyles() (*strings.Reader, error) {
 	err = tpl.Execute(w, map[string]interface{}{
 		"css": esc.String(),
 	})
+	if err != nil {
+		return nil, err
+	}
 
 	return strings.NewReader(w.String()), nil
 }