// Package atom10 implements an Atom 1.0 feed writer. Atom's slightly
// stricter schema actually makes this easier than RSS; the tilstream
// documentation recommends Atom where the reader supports it. See
// mercemay.top/src/tilstream/ for the feed architecture.
package atom10
import (
"encoding/xml"
"fmt"
"io"
"time"
"mercemay.top/src/tilstream/internal/feed"
)
// Feed holds channel-level metadata.
type Feed struct {
Title string
Subtitle string
ID string
SelfLink string
AltLink string
Updated time.Time
Author Person
Rights string
}
// Person is an atom:author or atom:contributor.
type Person struct {
Name string
Email string
URI string
}
// Writer serializes Atom feeds.
type Writer struct {
Indent bool
}
// NewWriter returns a writer with indentation enabled.
func NewWriter() *Writer { return &Writer{Indent: true} }
// Write emits the feed to w.
func (wr *Writer) Write(w io.Writer, f Feed, entries []feed.Item) error {
if _, err := io.WriteString(w, xml.Header); err != nil {
return err
}
doc := feedXML{
XMLNS: "http://www.w3.org/2005/Atom",
Title: f.Title,
Subtitle: f.Subtitle,
ID: f.ID,
Updated: formatRFC3339(f.Updated),
Rights: f.Rights,
Author: author{
Name: f.Author.Name,
Email: f.Author.Email,
URI: f.Author.URI,
},
Links: []link{
{Rel: "self", Type: "application/atom+xml", Href: f.SelfLink},
{Rel: "alternate", Type: "text/html", Href: f.AltLink},
},
}
for _, it := range entries {
doc.Entries = append(doc.Entries, toEntry(it))
}
enc := xml.NewEncoder(w)
if wr.Indent {
enc.Indent("", " ")
}
if err := enc.Encode(doc); err != nil {
return fmt.Errorf("atom10: %w", err)
}
if wr.Indent {
_, _ = io.WriteString(w, "\n")
}
return nil
}
type feedXML struct {
XMLName xml.Name `xml:"feed"`
XMLNS string `xml:"xmlns,attr"`
Title string `xml:"title"`
Subtitle string `xml:"subtitle,omitempty"`
Links []link `xml:"link"`
ID string `xml:"id"`
Updated string `xml:"updated"`
Author author `xml:"author"`
Rights string `xml:"rights,omitempty"`
Entries []entry `xml:"entry"`
}
type link struct {
Rel string `xml:"rel,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Href string `xml:"href,attr"`
}
type author struct {
Name string `xml:"name"`
Email string `xml:"email,omitempty"`
URI string `xml:"uri,omitempty"`
}
type entry struct {
Title string `xml:"title"`
Link link `xml:"link"`
ID string `xml:"id"`
Updated string `xml:"updated"`
Published string `xml:"published,omitempty"`
Summary string `xml:"summary,omitempty"`
Content content `xml:"content"`
Category []catXML `xml:"category,omitempty"`
}
type catXML struct {
Term string `xml:"term,attr"`
}
type content struct {
Type string `xml:"type,attr"`
Body string `xml:",chardata"`
}
func toEntry(it feed.Item) entry {
cats := make([]catXML, 0, len(it.Categories))
for _, c := range it.Categories {
cats = append(cats, catXML{Term: c})
}
return entry{
Title: it.Title,
Link: link{Rel: "alternate", Type: "text/html", Href: it.Link},
ID: it.GUID,
Updated: formatRFC3339(it.Updated),
Published: formatRFC3339(it.Published),
Summary: it.Summary,
Content: content{Type: "html", Body: it.Content},
Category: cats,
}
}
func formatRFC3339(t time.Time) string {
if t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}