From c4d5654e6360e90be2106439463f49acb55dffc8 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Sat, 29 Jun 2024 23:02:51 +0200 Subject: use encoding/xml for atom feeds --- internal/atom/atom.go | 36 ++++++++++++++++--- internal/builder/builder.go | 7 ++-- internal/builder/template.go | 85 +++++++++++++------------------------------- templates/feed.xml | 24 ------------- 4 files changed, 59 insertions(+), 93 deletions(-) delete mode 100644 templates/feed.xml diff --git a/internal/atom/atom.go b/internal/atom/atom.go index 601c125..f75d18a 100644 --- a/internal/atom/atom.go +++ b/internal/atom/atom.go @@ -1,7 +1,9 @@ package atom import ( + "bytes" "encoding/xml" + "net/url" "time" "go.alanpearce.eu/website/internal/config" @@ -11,18 +13,35 @@ func MakeTagURI(config *config.Config, specific string) string { return "tag:" + config.OriginalDomain + "," + config.DomainStartDate + ":" + specific } +func LinkXSL(w *bytes.Buffer, url string) error { + _, err := w.WriteString(``) + if err != nil { + return err + } + + return nil +} + type Link struct { XMLName xml.Name `xml:"link"` - Rel string `xml:"rel,attr"` - Type string `xml:"type,attr"` + Rel string `xml:"rel,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` Href string `xml:"href,attr"` } -func MakeLink(url string) Link { +func MakeLink(url *url.URL) Link { return Link{ Rel: "alternate", Type: "text/html", - Href: url, + Href: url.String(), } } @@ -41,3 +60,12 @@ type FeedEntry struct { Content FeedContent `xml:"content"` Author string `xml:"author>name"` } + +type Feed struct { + XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` + Title string `xml:"title"` + Link Link `xml:"link"` + ID string `xml:"id"` + Updated time.Time `xml:"updated"` + Entries []*FeedEntry `xml:"entry"` +} diff --git a/internal/builder/builder.go b/internal/builder/builder.go index b6da17d..63a9999 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -175,7 +175,6 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result, log.Debug("rendering tags feed", "tag", tag) feed, err := renderFeed( - ioConfig.Source, fmt.Sprintf("%s - %s", config.Title, tag), config, matchingPosts, @@ -184,7 +183,7 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result, if err != nil { return nil, errors.WithMessage(err, "could not render tag feed page") } - if err := outputToFile(feed, outDir, "tags", tag, "atom.xml"); err != nil { + if err := writerToFile(feed, outDir, "tags", tag, "atom.xml"); err != nil { return nil, err } } @@ -196,11 +195,11 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result, sitemap.AddPath("/post/", lastMod) log.Debug("rendering feed") - feed, err := renderFeed(ioConfig.Source, config.Title, config, posts, "feed") + feed, err := renderFeed(config.Title, config, posts, "feed") if err != nil { return nil, errors.WithMessage(err, "could not render feed") } - if err := outputToFile(feed, outDir, "atom.xml"); err != nil { + if err := writerToFile(feed, outDir, "atom.xml"); err != nil { return nil, err } diff --git a/internal/builder/template.go b/internal/builder/template.go index 957ea60..9f019df 100644 --- a/internal/builder/template.go +++ b/internal/builder/template.go @@ -1,6 +1,7 @@ package builder import ( + "bytes" "encoding/xml" "io" "os" @@ -19,9 +20,8 @@ import ( ) 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", @@ -36,19 +36,6 @@ func loadCSS(source string) { 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 } @@ -87,43 +74,31 @@ func renderRobotsTXT(source string, config *config.Config) (io.Reader, error) { } func renderFeed( - source string, title string, config *config.Config, posts []content.Post, specific string, -) (io.Reader, error) { - reader, err := loadTemplate(filepath.Join(source, "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, @@ -132,27 +107,15 @@ 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(source string) (*strings.Reader, error) { diff --git a/templates/feed.xml b/templates/feed.xml deleted file mode 100644 index ddc90dd..0000000 --- a/templates/feed.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - Example Feed - - urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 - 2003-12-13T18:30:02Z - - Atom-Powered Robots Run Amok - - urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a - 2003-12-13T18:30:02Z - Some text. - -
-

This is the entry content.

-
-
- - John Doe - -
- -
-- cgit 1.4.1