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(filenames ...string) (string, error) { fn := filepath.Join(filenames...) f, err := os.Open(fn) if err != nil { return "", err } defer f.Close() doc, err := NewDocumentFromReader(f) if err != nil { return "", err } html := doc.Find("head > style").Text() return hash(html), nil }