about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2024-06-29 23:02:51 +0200
committerAlan Pearce2024-06-29 23:02:51 +0200
commitc4d5654e6360e90be2106439463f49acb55dffc8 (patch)
tree9056a81b9733022f17ce7dc2eff070bedf7822ea
parente1a16b18ee0271bbad754d889a1455fc2e8b1d6d (diff)
downloadwebsite-c4d5654e6360e90be2106439463f49acb55dffc8.tar.lz
website-c4d5654e6360e90be2106439463f49acb55dffc8.tar.zst
website-c4d5654e6360e90be2106439463f49acb55dffc8.zip
use encoding/xml for atom feeds
-rw-r--r--internal/atom/atom.go36
-rw-r--r--internal/builder/builder.go7
-rw-r--r--internal/builder/template.go85
-rw-r--r--templates/feed.xml24
4 files changed, 59 insertions, 93 deletions
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(`<?xml-stylesheet href="`)
+	if err != nil {
+		return err
+	}
+	err = xml.EscapeText(w, []byte(url))
+	if err != nil {
+		return err
+	}
+	_, err = w.WriteString(`" type="text/xsl"?>`)
+	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 @@
-<?xml version="1.0" encoding="utf-8"?>
-<?xml-stylesheet href="/feed-styles.xsl" type="text/xsl"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
-  <title>Example Feed</title>
-  <link href="http://example.org/"></link>
-  <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
-  <updated>2003-12-13T18:30:02Z</updated>
-  <entry>
-    <title>Atom-Powered Robots Run Amok</title>
-    <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"></link>
-    <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
-    <updated>2003-12-13T18:30:02Z</updated>
-    <summary>Some text.</summary>
-    <content type="html">
-      <div>
-        <p>This is the entry content.</p>
-      </div>
-    </content>
-    <author>
-      <name>John Doe</name> 
-    </author>
-  </entry>
-
-</feed>