diff options
Diffstat (limited to 'internal/builder')
-rw-r--r-- | internal/builder/builder.go | 205 | ||||
-rw-r--r-- | internal/builder/hasher.go | 13 | ||||
-rw-r--r-- | internal/builder/template.go | 166 |
3 files changed, 0 insertions, 384 deletions
diff --git a/internal/builder/builder.go b/internal/builder/builder.go deleted file mode 100644 index df66f87..0000000 --- a/internal/builder/builder.go +++ /dev/null @@ -1,205 +0,0 @@ -package builder - -import ( - "fmt" - "path" - "path/filepath" - "slices" - "time" - - "go.alanpearce.eu/website/internal/config" - "go.alanpearce.eu/website/internal/content" - "go.alanpearce.eu/website/internal/sitemap" - "go.alanpearce.eu/website/internal/storage/files" - "go.alanpearce.eu/website/templates" - "go.alanpearce.eu/x/log" - - mapset "github.com/deckarep/golang-set/v2" - "gitlab.com/tozd/go/errors" -) - -type IOConfig struct { - Source string `conf:"default:.,short:s,flag:src"` - Destination string `conf:"default:public,short:d,flag:dest"` - Development bool `conf:"default:false,flag:dev"` -} - -type Result struct { - Hashes []string -} - -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) { - storage := files.NewWriter(ioConfig.Destination, log, &files.Options{ - Compress: !ioConfig.Development, - }) - - joinSource := joinSourcePath(ioConfig.Source) - - log.Debug("output", "dir", ioConfig.Destination) - r := &Result{ - Hashes: make([]string, 0), - } - - err := storage.CopyRecursive(joinSource("static")) - if err != nil { - return nil, errors.WithMessage(err, "could not copy static files") - } - - log.Debug("reading posts") - posts, tags, err := content.ReadPosts(&content.Config{ - Root: joinSource("content"), - InputDir: "post", - }, log.Named("content")) - if err != nil { - return nil, err - } - - sitemap := sitemap.New(config) - lastMod := time.Now() - if len(posts) > 0 { - lastMod = posts[0].Date - } - - for _, post := range posts { - log.Debug("rendering post", "post", post.Basename) - sitemap.AddPath(post.URL, post.Date) - if err := storage.RenderToFile(templates.PostPage(config, post), post.Output); err != nil { - return nil, err - } - } - - log.Debug("rendering tags list") - if err := storage.RenderToFile( - templates.TagsPage(config, "tags", mapset.Sorted(tags), "/tags"), - "tags/", - ); err != nil { - return nil, err - } - sitemap.AddPath("/tags/", lastMod) - - for _, tag := range tags.ToSlice() { - matchingPosts := []content.Post{} - for _, post := range posts { - if slices.Contains(post.Taxonomies.Tags, tag) { - matchingPosts = append(matchingPosts, post) - } - } - log.Debug("rendering tags page", "tag", tag) - url := "/tags/" + tag - if err := storage.RenderToFile( - templates.TagPage(config, tag, matchingPosts, url), - path.Join("tags", tag)+"/", - ); err != nil { - return nil, err - } - sitemap.AddPath(url, matchingPosts[0].Date) - - log.Debug("rendering tags feed", "tag", tag) - feed, err := renderFeed( - fmt.Sprintf("%s - %s", config.Title, tag), - config, - matchingPosts, - tag, - ) - if err != nil { - return nil, errors.WithMessage(err, "could not render tag feed page") - } - if err := storage.WriterToFile(feed, path.Join("tags", tag, "atom.xml")); err != nil { - return nil, err - } - } - - log.Debug("rendering list page") - if err := storage.RenderToFile( - templates.ListPage(config, posts, "/post"), - "post/", - ); err != nil { - return nil, err - } - sitemap.AddPath("/post/", lastMod) - - log.Debug("rendering feed") - feed, err := renderFeed(config.Title, config, posts, "feed") - if err != nil { - return nil, errors.WithMessage(err, "could not render feed") - } - if err := storage.WriterToFile(feed, "atom.xml"); err != nil { - return nil, err - } - - log.Debug("rendering feed styles") - feedStyles, err := renderFeedStyles(ioConfig.Source) - if err != nil { - return nil, errors.WithMessage(err, "could not render feed styles") - } - if err := storage.OutputToFile(feedStyles, "feed-styles.xsl"); err != nil { - return nil, err - } - _, err = feedStyles.Seek(0, 0) - if err != nil { - return nil, err - } - h, err := getFeedStylesHash(feedStyles) - if err != nil { - return nil, err - } - r.Hashes = append(r.Hashes, h) - - log.Debug("rendering homepage") - _, text, err := content.GetPost(joinSource(filepath.Join("content", "index.md"))) - if err != nil { - return nil, err - } - content, err := content.RenderMarkdown(text) - if err != nil { - return nil, err - } - if err := storage.RenderToFile(templates.Homepage(config, posts, content), "/"); 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{}) - f, err := storage.Open("/") - if err != nil { - return nil, err - } - defer f.Close() - h, _ = getHTMLStyleHash(f) - r.Hashes = append(r.Hashes, h) - - log.Debug("rendering sitemap") - if err := storage.WriterToFile(sitemap, "sitemap.xml"); err != nil { - return nil, err - } - - log.Debug("rendering robots.txt") - rob, err := renderRobotsTXT(ioConfig.Source, config) - if err != nil { - return nil, err - } - if err := storage.OutputToFile(rob, "robots.txt"); err != nil { - return nil, err - } - - return r, nil -} - -func BuildSite(ioConfig *IOConfig, cfg *config.Config, log *log.Logger) (*Result, error) { - if cfg == nil { - return nil, errors.New("config is nil") - } - cfg.InjectLiveReload = ioConfig.Development - - templates.Setup() - loadCSS(ioConfig.Source) - - return build(ioConfig, cfg, log) -} diff --git a/internal/builder/hasher.go b/internal/builder/hasher.go deleted file mode 100644 index f0f9167..0000000 --- a/internal/builder/hasher.go +++ /dev/null @@ -1,13 +0,0 @@ -package builder - -import ( - "crypto/sha256" - "encoding/base64" -) - -func hash(s string) string { - shasum := sha256.New() - shasum.Write([]byte(s)) - - return "sha256-" + base64.StdEncoding.EncodeToString(shasum.Sum(nil)) -} diff --git a/internal/builder/template.go b/internal/builder/template.go deleted file mode 100644 index 914b67d..0000000 --- a/internal/builder/template.go +++ /dev/null @@ -1,166 +0,0 @@ -package builder - -import ( - "bytes" - "encoding/xml" - "io" - "os" - "path/filepath" - "strings" - "text/template" - - "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" - "gitlab.com/tozd/go/errors" -) - -var ( - 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(source string) { - bytes, err := os.ReadFile(filepath.Join(source, "templates/style.css")) - if err != nil { - panic(err) - } - css = string(bytes) -} - -type QuerySelection struct { - *goquery.Selection -} - -type QueryDocument struct { - *goquery.Document -} - -func NewDocumentFromReader(r io.Reader) (*QueryDocument, error) { - doc, err := goquery.NewDocumentFromReader(r) - - return &QueryDocument{doc}, errors.WithMessage(err, "could not create query document") -} - -func (q *QueryDocument) Find(selector string) *QuerySelection { - return &QuerySelection{q.Document.Find(selector)} -} - -func renderRobotsTXT(source string, config *config.Config) (io.Reader, error) { - r, w := io.Pipe() - tpl, err := template.ParseFiles(filepath.Join(source, "templates/robots.tmpl")) - if err != nil { - return nil, err - } - go func() { - err = tpl.Execute(w, map[string]interface{}{ - "BaseURL": config.BaseURL, - }) - if err != nil { - w.CloseWithError(err) - } - w.Close() - }() - - return r, nil -} - -func renderFeed( - title string, - config *config.Config, - posts []content.Post, - specific string, -) (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 - } - feed := &atom.Feed{ - Title: title, - Link: atom.MakeLink(config.BaseURL.URL), - ID: atom.MakeTagURI(config, specific), - Updated: datetime, - Entries: make([]*atom.FeedEntry, len(posts)), - } - - for i, post := range posts { - feed.Entries[i] = &atom.FeedEntry{ - Title: post.Title, - Link: atom.MakeLink(config.BaseURL.JoinPath(post.URL)), - ID: atom.MakeTagURI(config, post.Basename), - Updated: post.Date.UTC(), - Summary: post.Description, - Author: config.Title, - Content: atom.FeedContent{ - Content: post.Content, - Type: "html", - }, - } - } - enc := xml.NewEncoder(buf) - err = enc.Encode(feed) - if err != nil { - return nil, err - } - - return buf, nil -} - -func renderFeedStyles(source string) (*strings.Reader, error) { - tpl, err := template.ParseFiles(filepath.Join(source, "templates/feed-styles.xsl")) - if err != nil { - return nil, err - } - - esc := &strings.Builder{} - err = xml.EscapeText(esc, []byte(css)) - if err != nil { - return nil, err - } - - w := &strings.Builder{} - err = tpl.Execute(w, map[string]interface{}{ - "css": esc.String(), - }) - if err != nil { - return nil, err - } - - return strings.NewReader(w.String()), nil -} - -func getFeedStylesHash(r io.Reader) (string, error) { - doc, err := xmlquery.Parse(r) - if err != nil { - return "", err - } - expr, err := xpath.CompileWithNS("//xhtml:style", nsMap) - if err != nil { - return "", errors.WithMessage(err, "could not parse XPath") - } - style := xmlquery.QuerySelector(doc, expr) - - return hash(style.InnerText()), nil -} - -func getHTMLStyleHash(r io.Reader) (string, error) { - doc, err := NewDocumentFromReader(r) - if err != nil { - return "", err - } - html := doc.Find("head > style").Text() - - return hash(html), nil -} |