// Package jsonfeed writes JSON Feed 1.1 documents, as specified at
// jsonfeed.org. I added this after realizing the JSON Feed reader I use
// most often didn't respect RSS 2.0 GUIDs.
//
// mercemay.top/src/tilstream/ has an overview of all three writers.
package jsonfeed
import (
"encoding/json"
"io"
"time"
"mercemay.top/src/tilstream/internal/feed"
)
// Feed is a top-level JSON Feed document.
type Feed struct {
Version string `json:"version"`
Title string `json:"title"`
HomePageURL string `json:"home_page_url,omitempty"`
FeedURL string `json:"feed_url,omitempty"`
Description string `json:"description,omitempty"`
Language string `json:"language,omitempty"`
Authors []Author `json:"authors,omitempty"`
Icon string `json:"icon,omitempty"`
Favicon string `json:"favicon,omitempty"`
Items []Item `json:"items"`
}
// Author mirrors the spec's author object.
type Author struct {
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
Avatar string `json:"avatar,omitempty"`
}
// Item is a single feed entry.
type Item struct {
ID string `json:"id"`
URL string `json:"url,omitempty"`
Title string `json:"title,omitempty"`
ContentHTML string `json:"content_html,omitempty"`
ContentText string `json:"content_text,omitempty"`
Summary string `json:"summary,omitempty"`
DatePublished string `json:"date_published,omitempty"`
DateModified string `json:"date_modified,omitempty"`
Authors []Author `json:"authors,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// Writer serializes JSON Feeds.
type Writer struct {
Pretty bool
}
// NewWriter returns a writer with pretty-printing enabled.
func NewWriter() *Writer { return &Writer{Pretty: true} }
// Write emits the feed to w.
func (wr *Writer) Write(w io.Writer, f Feed, items []feed.Item) error {
f.Version = "https://jsonfeed.org/version/1.1"
if f.Items == nil {
f.Items = make([]Item, 0, len(items))
}
for _, it := range items {
f.Items = append(f.Items, FromFeedItem(it))
}
enc := json.NewEncoder(w)
if wr.Pretty {
enc.SetIndent("", " ")
}
enc.SetEscapeHTML(false)
return enc.Encode(f)
}
// FromFeedItem converts our internal feed.Item into a JSON Feed Item.
func FromFeedItem(it feed.Item) Item {
return Item{
ID: it.GUID,
URL: it.Link,
Title: it.Title,
ContentHTML: it.Content,
Summary: it.Summary,
DatePublished: formatTime(it.Published),
DateModified: formatTime(it.Updated),
Tags: append([]string(nil), it.Categories...),
Authors: authorsFromName(it.Author),
}
}
func authorsFromName(name string) []Author {
if name == "" {
return nil
}
return []Author{{Name: name}}
}
func formatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}
// Decode reads a JSON Feed document from r. Useful for round-trip tests.
func Decode(r io.Reader) (Feed, error) {
var f Feed
if err := json.NewDecoder(r).Decode(&f); err != nil {
return Feed{}, err
}
return f, nil
}