~evanj/evanjon.es

80ee5ef1c9b8af3c510129cec6810bd10942df77 — Evan M Jones 1 year, 4 months ago d296b66
Feat(rss): RSS feed has been added back.
M evanjon.es.go => evanjon.es.go +22 -19
@@ 14,6 14,9 @@ import (

	"git.sr.ht/~evanj/evanjon.es/internal/s/cms"
	api "git.sr.ht/~evanj/evanjon.es/pkg/cms"

	"git.sr.ht/~evanj/evanjon.es/internal/c/rss"
	rssencoder "git.sr.ht/~evanj/evanjon.es/internal/s/rss"
)

//go:generate go get git.sr.ht/~evanj/embed


@@ 78,22 81,22 @@ func New(out io.Writer) *App {
			typeMeta,
		),

		// cacheRouter: cms,

		// rssRouter: rss.New(
		// 	e,
		// 	log.New(out, "[evanjon.es:rss] ", 0),
		// 	cms,
		// 	rssencoder.New(
		// 		"evanjon.es",
		// 		"My personal corner of th WWW.",
		// 		"https://evanjon.es/",
		// 		"Evan Jones",
		// 		"me@evanjon.es",
		// 	),
		// 	typePage,
		// 	typePost,
		// ),
		cacheRouter: cms,

		rssRouter: rss.New(
			e,
			log.New(out, "[evanjon.es:rss] ", 0),
			cms,
			rssencoder.New(
				"evanjon.es",
				"My personal corner of th WWW.",
				"https://evanjon.es/",
				"Evan Jones",
				"me@evanjon.es",
			),
			typePage,
			typePost,
		),
	}
}



@@ 109,9 112,9 @@ func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "/ping":
		app.baseRouter.ServeHTTP(w, r)
		break
	// case "/rss":
	// 	app.rssRouter.ServeHTTP(w, r)
	// 	break
	case "/rss":
		app.rssRouter.ServeHTTP(w, r)
		break
	case "/blog":
		fallthrough
	case "/posts":

M go.mod => go.mod +1 -0
@@ 4,5 4,6 @@ go 1.14

require (
	git.sr.ht/~evanj/embed v0.0.0-20200525225021-2cde7dae7bfa // indirect
	github.com/gorilla/feeds v1.1.1
	github.com/tdewolff/minify/v2 v2.7.3
)

M go.sum => go.sum +2 -0
@@ 4,6 4,8 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=

M internal/c/rss/rss.go => internal/c/rss/rss.go +58 -58
@@ 1,60 1,60 @@
package rss

// import (
// 	"context"
// 	"log"
// 	"net/http"
//
// 	"git.sr.ht/~evanj/evanjon.es/internal/c"
// 	"git.sr.ht/~evanj/evanjon.es/internal/m/content"
// )
//
// type ListEndpoint struct {
// 	*c.Endpoint
// 	log                *log.Logger
// 	cms                cmsProvider
// 	rss                rssProvider
// 	typePage, typePost int
// }
//
// type cmsProvider interface {
// 	List(ctx context.Context, typeID int) ([]content.Content, error)
// }
//
// type rssProvider interface {
// 	List(items []content.Content) (string, error)
// }
//
// func New(base *c.Endpoint, log *log.Logger, cms cmsProvider, rss rssProvider, typePage, typePost int) *ListEndpoint {
// 	return &ListEndpoint{
// 		base,
// 		log,
// 		cms,
// 		rss,
// 		typePage, typePost,
// 	}
// }
//
// func (e *ListEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 	ctx := e.ContextTimeout(r)
//
// 	l, err := e.cms.List(ctx, e.typePost)
// 	if err != nil {
// 		e.log.Println(err)
// 		w.WriteHeader(http.StatusNotFound)
// 		w.Write([]byte("failed to find content"))
// 		return
// 	}
//
// 	feed, err := e.rss.List(l)
// 	if err != nil {
// 		e.log.Println(err)
// 		w.WriteHeader(http.StatusNotFound)
// 		w.Write([]byte("failed to generate feed"))
// 		return
// 	}
//
// 	w.WriteHeader(http.StatusOK)
// 	w.Header().Add("Content-Type", "application/rss+xml")
// 	w.Write([]byte(feed))
// }
import (
	"context"
	"log"
	"net/http"

	"git.sr.ht/~evanj/evanjon.es/internal/c"
	"git.sr.ht/~evanj/evanjon.es/pkg/cms"
)

type ListEndpoint struct {
	*c.Endpoint
	log                *log.Logger
	cms                cmsProvider
	rss                rssProvider
	typePage, typePost int
}

type cmsProvider interface {
	List(ctx context.Context, typeID int, order, field string) ([]cms.Content, error)
}

type rssProvider interface {
	List(items []cms.Content) (string, error)
}

func New(base *c.Endpoint, log *log.Logger, cms cmsProvider, rss rssProvider, typePage, typePost int) *ListEndpoint {
	return &ListEndpoint{
		base,
		log,
		cms,
		rss,
		typePage, typePost,
	}
}

func (e *ListEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := e.ContextTimeout(r)

	l, err := e.cms.List(ctx, e.typePost, "desc", "date")
	if err != nil {
		e.log.Println(err)
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte("failed to find content"))
		return
	}

	feed, err := e.rss.List(l)
	if err != nil {
		e.log.Println(err)
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte("failed to generate feed"))
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Header().Add("Content-Type", "application/rss+xml")
	w.Write([]byte(feed))
}

M internal/s/rss/rss.go => internal/s/rss/rss.go +36 -36
@@ 1,38 1,38 @@
package rss

// import (
// 	"git.sr.ht/~evanj/evanjon.es/internal/m/content"
// 	"github.com/gorilla/feeds"
// )
//
// type rss struct {
// 	title, desc, link, author, email string
// }
//
// func New(title, desc, link, author, email string) rss {
// 	return rss{title, desc, link, author, email}
// }
//
// func (r rss) rss(item content.Content) *feeds.Item {
// 	return &feeds.Item{
// 		Title:       item.Name(),
// 		Description: string(item.Desc()),
// 		Author:      &feeds.Author{Name: r.author, Email: r.email},
// 		Link:        &feeds.Link{Href: r.link + item.Slug()},
// 	}
// }
//
// func (r rss) List(items []content.Content) (string, error) {
// 	feed := &feeds.Feed{
// 		Title:       r.title,
// 		Link:        &feeds.Link{Href: r.link},
// 		Description: r.desc,
// 		Author:      &feeds.Author{Name: r.author, Email: r.email},
// 	}
//
// 	for _, item := range items {
// 		feed.Items = append(feed.Items, r.rss(item))
// 	}
//
// 	return feed.ToRss()
// }
import (
	"git.sr.ht/~evanj/evanjon.es/pkg/cms"
	"github.com/gorilla/feeds"
)

type rss struct {
	title, desc, link, author, email string
}

func New(title, desc, link, author, email string) rss {
	return rss{title, desc, link, author, email}
}

func (r rss) rss(c cms.ContentMust) *feeds.Item {
	return &feeds.Item{
		Title:       c.Val("name"),
		Description: c.Val("desc"),
		Author:      &feeds.Author{Name: r.author, Email: r.email},
		Link:        &feeds.Link{Href: r.link + c.Val("slug")},
	}
}

func (r rss) List(items []cms.Content) (string, error) {
	feed := &feeds.Feed{
		Title:       r.title,
		Link:        &feeds.Link{Href: r.link},
		Description: r.desc,
		Author:      &feeds.Author{Name: r.author, Email: r.email},
	}

	for _, item := range items {
		feed.Items = append(feed.Items, r.rss(item.Must()))
	}

	return feed.ToRss()
}

A vendor/github.com/gorilla/feeds/.travis.yml => vendor/github.com/gorilla/feeds/.travis.yml +16 -0
@@ 0,0 1,16 @@
language: go
sudo: false
matrix:
  include:
    - go: 1.8
    - go: 1.9
    - go: "1.10"
    - go: 1.x
    - go: tip
  allow_failures:
    - go: tip
script:
  - go get -t -v ./...
  - diff -u <(echo -n) <(gofmt -d -s .)
  - go vet .
  - go test -v -race ./...

A vendor/github.com/gorilla/feeds/AUTHORS => vendor/github.com/gorilla/feeds/AUTHORS +29 -0
@@ 0,0 1,29 @@
# This is the official list of gorilla/feeds authors for copyright purposes.
# Please keep the list sorted.

Dmitry Chestnykh <dmitry@codingrobots.com>
Eddie Scholtz <eascholtz@gmail.com>
Gabriel Simmer <bladesimmer@gmail.com>
Google LLC (https://opensource.google.com/)
honky <honky@defendtheplanet.net>
James Gregory <james@jagregory.com>
Jason Hall <imjasonh@gmail.com>
Jason Moiron <jmoiron@jmoiron.net>
Kamil Kisiel <kamil@kamilkisiel.net>
Kevin Stock <kevinstock@tantalic.com>
Markus Zimmermann <markus.zimmermann@nethead.at>
Matt Silverlock <matt@eatsleeprepeat.net>
Matthew Dawson <matthew@mjdsystems.ca>
Milan Aleksic <milanaleksic@gmail.com>
Milan Aleksić <milanaleksic@gmail.com>
nlimpid <jshuangzl@gmail.com>
Paul Petring <paul@defendtheplanet.net>
Sean Enck <enckse@users.noreply.github.com>
Sue Spence <virtuallysue@gmail.com>
Supermighty <ukiah@faction.com>
Toru Fukui <fukuimone@gmail.com>
Vabd <vabd@anon.acme>
Volker <lists.volker@gmail.com>
ZhiFeng Hu <hufeng1987@gmail.com>
weberc2 <weberc2@gmail.com>


A vendor/github.com/gorilla/feeds/LICENSE => vendor/github.com/gorilla/feeds/LICENSE +22 -0
@@ 0,0 1,22 @@
Copyright (c) 2013-2018 The Gorilla Feeds Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

  Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

A vendor/github.com/gorilla/feeds/README.md => vendor/github.com/gorilla/feeds/README.md +185 -0
@@ 0,0 1,185 @@
## gorilla/feeds
[![GoDoc](https://godoc.org/github.com/gorilla/feeds?status.svg)](https://godoc.org/github.com/gorilla/feeds)
[![Build Status](https://travis-ci.org/gorilla/feeds.svg?branch=master)](https://travis-ci.org/gorilla/feeds)

feeds is a web feed generator library for generating RSS, Atom and JSON feeds from Go
applications.

### Goals

 * Provide a simple interface to create both Atom & RSS 2.0 feeds
 * Full support for [Atom][atom], [RSS 2.0][rss], and [JSON Feed Version 1][jsonfeed] spec elements
 * Ability to modify particulars for each spec

[atom]: https://tools.ietf.org/html/rfc4287
[rss]: http://www.rssboard.org/rss-specification
[jsonfeed]: https://jsonfeed.org/version/1

### Usage

```go
package main

import (
    "fmt"
    "log"
    "time"
    "github.com/gorilla/feeds"
)

func main() {
    now := time.Now()
    feed := &feeds.Feed{
        Title:       "jmoiron.net blog",
        Link:        &feeds.Link{Href: "http://jmoiron.net/blog"},
        Description: "discussion about tech, footie, photos",
        Author:      &feeds.Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
        Created:     now,
    }

    feed.Items = []*feeds.Item{
        &feeds.Item{
            Title:       "Limiting Concurrency in Go",
            Link:        &feeds.Link{Href: "http://jmoiron.net/blog/limiting-concurrency-in-go/"},
            Description: "A discussion on controlled parallelism in golang",
            Author:      &feeds.Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
            Created:     now,
        },
        &feeds.Item{
            Title:       "Logic-less Template Redux",
            Link:        &feeds.Link{Href: "http://jmoiron.net/blog/logicless-template-redux/"},
            Description: "More thoughts on logicless templates",
            Created:     now,
        },
        &feeds.Item{
            Title:       "Idiomatic Code Reuse in Go",
            Link:        &feeds.Link{Href: "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"},
            Description: "How to use interfaces <em>effectively</em>",
            Created:     now,
        },
    }

    atom, err := feed.ToAtom()
    if err != nil {
        log.Fatal(err)
    }

    rss, err := feed.ToRss()
    if err != nil {
        log.Fatal(err)
    }

    json, err := feed.ToJSON()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(atom, "\n", rss, "\n", json)
}
```

Outputs:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>jmoiron.net blog</title>
  <link href="http://jmoiron.net/blog"></link>
  <id>http://jmoiron.net/blog</id>
  <updated>2013-01-16T03:26:01-05:00</updated>
  <summary>discussion about tech, footie, photos</summary>
  <entry>
    <title>Limiting Concurrency in Go</title>
    <link href="http://jmoiron.net/blog/limiting-concurrency-in-go/"></link>
    <updated>2013-01-16T03:26:01-05:00</updated>
    <id>tag:jmoiron.net,2013-01-16:/blog/limiting-concurrency-in-go/</id>
    <summary type="html">A discussion on controlled parallelism in golang</summary>
    <author>
      <name>Jason Moiron</name>
      <email>jmoiron@jmoiron.net</email>
    </author>
  </entry>
  <entry>
    <title>Logic-less Template Redux</title>
    <link href="http://jmoiron.net/blog/logicless-template-redux/"></link>
    <updated>2013-01-16T03:26:01-05:00</updated>
    <id>tag:jmoiron.net,2013-01-16:/blog/logicless-template-redux/</id>
    <summary type="html">More thoughts on logicless templates</summary>
    <author></author>
  </entry>
  <entry>
    <title>Idiomatic Code Reuse in Go</title>
    <link href="http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"></link>
    <updated>2013-01-16T03:26:01-05:00</updated>
    <id>tag:jmoiron.net,2013-01-16:/blog/idiomatic-code-reuse-in-go/</id>
    <summary type="html">How to use interfaces &lt;em&gt;effectively&lt;/em&gt;</summary>
    <author></author>
  </entry>
</feed>

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>jmoiron.net blog</title>
    <link>http://jmoiron.net/blog</link>
    <description>discussion about tech, footie, photos</description>
    <managingEditor>jmoiron@jmoiron.net (Jason Moiron)</managingEditor>
    <pubDate>2013-01-16T03:22:24-05:00</pubDate>
    <item>
      <title>Limiting Concurrency in Go</title>
      <link>http://jmoiron.net/blog/limiting-concurrency-in-go/</link>
      <description>A discussion on controlled parallelism in golang</description>
      <pubDate>2013-01-16T03:22:24-05:00</pubDate>
    </item>
    <item>
      <title>Logic-less Template Redux</title>
      <link>http://jmoiron.net/blog/logicless-template-redux/</link>
      <description>More thoughts on logicless templates</description>
      <pubDate>2013-01-16T03:22:24-05:00</pubDate>
    </item>
    <item>
      <title>Idiomatic Code Reuse in Go</title>
      <link>http://jmoiron.net/blog/idiomatic-code-reuse-in-go/</link>
      <description>How to use interfaces &lt;em&gt;effectively&lt;/em&gt;</description>
      <pubDate>2013-01-16T03:22:24-05:00</pubDate>
    </item>
  </channel>
</rss>

{
  "version": "https://jsonfeed.org/version/1",
  "title": "jmoiron.net blog",
  "home_page_url": "http://jmoiron.net/blog",
  "description": "discussion about tech, footie, photos",
  "author": {
    "name": "Jason Moiron"
  },
  "items": [
    {
      "id": "",
      "url": "http://jmoiron.net/blog/limiting-concurrency-in-go/",
      "title": "Limiting Concurrency in Go",
      "summary": "A discussion on controlled parallelism in golang",
      "date_published": "2013-01-16T03:22:24.530817846-05:00",
      "author": {
        "name": "Jason Moiron"
      }
    },
    {
      "id": "",
      "url": "http://jmoiron.net/blog/logicless-template-redux/",
      "title": "Logic-less Template Redux",
      "summary": "More thoughts on logicless templates",
      "date_published": "2013-01-16T03:22:24.530817846-05:00"
    },
    {
      "id": "",
      "url": "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/",
      "title": "Idiomatic Code Reuse in Go",
      "summary": "How to use interfaces \u003cem\u003eeffectively\u003c/em\u003e",
      "date_published": "2013-01-16T03:22:24.530817846-05:00"
    }
  ]
}
```


A vendor/github.com/gorilla/feeds/atom.go => vendor/github.com/gorilla/feeds/atom.go +169 -0
@@ 0,0 1,169 @@
package feeds

import (
	"encoding/xml"
	"fmt"
	"net/url"
	"time"
)

// Generates Atom feed as XML

const ns = "http://www.w3.org/2005/Atom"

type AtomPerson struct {
	Name  string `xml:"name,omitempty"`
	Uri   string `xml:"uri,omitempty"`
	Email string `xml:"email,omitempty"`
}

type AtomSummary struct {
	XMLName xml.Name `xml:"summary"`
	Content string   `xml:",chardata"`
	Type    string   `xml:"type,attr"`
}

type AtomContent struct {
	XMLName xml.Name `xml:"content"`
	Content string   `xml:",chardata"`
	Type    string   `xml:"type,attr"`
}

type AtomAuthor struct {
	XMLName xml.Name `xml:"author"`
	AtomPerson
}

type AtomContributor struct {
	XMLName xml.Name `xml:"contributor"`
	AtomPerson
}

type AtomEntry struct {
	XMLName     xml.Name `xml:"entry"`
	Xmlns       string   `xml:"xmlns,attr,omitempty"`
	Title       string   `xml:"title"`   // required
	Updated     string   `xml:"updated"` // required
	Id          string   `xml:"id"`      // required
	Category    string   `xml:"category,omitempty"`
	Content     *AtomContent
	Rights      string `xml:"rights,omitempty"`
	Source      string `xml:"source,omitempty"`
	Published   string `xml:"published,omitempty"`
	Contributor *AtomContributor
	Links       []AtomLink   // required if no child 'content' elements
	Summary     *AtomSummary // required if content has src or content is base64
	Author      *AtomAuthor  // required if feed lacks an author
}

// Multiple links with different rel can coexist
type AtomLink struct {
	//Atom 1.0 <link rel="enclosure" type="audio/mpeg" title="MP3" href="http://www.example.org/myaudiofile.mp3" length="1234" />
	XMLName xml.Name `xml:"link"`
	Href    string   `xml:"href,attr"`
	Rel     string   `xml:"rel,attr,omitempty"`
	Type    string   `xml:"type,attr,omitempty"`
	Length  string   `xml:"length,attr,omitempty"`
}

type AtomFeed struct {
	XMLName     xml.Name `xml:"feed"`
	Xmlns       string   `xml:"xmlns,attr"`
	Title       string   `xml:"title"`   // required
	Id          string   `xml:"id"`      // required
	Updated     string   `xml:"updated"` // required
	Category    string   `xml:"category,omitempty"`
	Icon        string   `xml:"icon,omitempty"`
	Logo        string   `xml:"logo,omitempty"`
	Rights      string   `xml:"rights,omitempty"` // copyright used
	Subtitle    string   `xml:"subtitle,omitempty"`
	Link        *AtomLink
	Author      *AtomAuthor `xml:"author,omitempty"`
	Contributor *AtomContributor
	Entries     []*AtomEntry `xml:"entry"`
}

type Atom struct {
	*Feed
}

func newAtomEntry(i *Item) *AtomEntry {
	id := i.Id
	// assume the description is html
	s := &AtomSummary{Content: i.Description, Type: "html"}

	if len(id) == 0 {
		// if there's no id set, try to create one, either from data or just a uuid
		if len(i.Link.Href) > 0 && (!i.Created.IsZero() || !i.Updated.IsZero()) {
			dateStr := anyTimeFormat("2006-01-02", i.Updated, i.Created)
			host, path := i.Link.Href, "/invalid.html"
			if url, err := url.Parse(i.Link.Href); err == nil {
				host, path = url.Host, url.Path
			}
			id = fmt.Sprintf("tag:%s,%s:%s", host, dateStr, path)
		} else {
			id = "urn:uuid:" + NewUUID().String()
		}
	}
	var name, email string
	if i.Author != nil {
		name, email = i.Author.Name, i.Author.Email
	}

	link_rel := i.Link.Rel
	if link_rel == "" {
		link_rel = "alternate"
	}
	x := &AtomEntry{
		Title:   i.Title,
		Links:   []AtomLink{{Href: i.Link.Href, Rel: link_rel, Type: i.Link.Type}},
		Id:      id,
		Updated: anyTimeFormat(time.RFC3339, i.Updated, i.Created),
		Summary: s,
	}

	// if there's a content, assume it's html
	if len(i.Content) > 0 {
		x.Content = &AtomContent{Content: i.Content, Type: "html"}
	}

	if i.Enclosure != nil && link_rel != "enclosure" {
		x.Links = append(x.Links, AtomLink{Href: i.Enclosure.Url, Rel: "enclosure", Type: i.Enclosure.Type, Length: i.Enclosure.Length})
	}

	if len(name) > 0 || len(email) > 0 {
		x.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: name, Email: email}}
	}
	return x
}

// create a new AtomFeed with a generic Feed struct's data
func (a *Atom) AtomFeed() *AtomFeed {
	updated := anyTimeFormat(time.RFC3339, a.Updated, a.Created)
	feed := &AtomFeed{
		Xmlns:    ns,
		Title:    a.Title,
		Link:     &AtomLink{Href: a.Link.Href, Rel: a.Link.Rel},
		Subtitle: a.Description,
		Id:       a.Link.Href,
		Updated:  updated,
		Rights:   a.Copyright,
	}
	if a.Author != nil {
		feed.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: a.Author.Name, Email: a.Author.Email}}
	}
	for _, e := range a.Items {
		feed.Entries = append(feed.Entries, newAtomEntry(e))
	}
	return feed
}

// FeedXml returns an XML-Ready object for an Atom object
func (a *Atom) FeedXml() interface{} {
	return a.AtomFeed()
}

// FeedXml returns an XML-ready object for an AtomFeed object
func (a *AtomFeed) FeedXml() interface{} {
	return a
}

A vendor/github.com/gorilla/feeds/doc.go => vendor/github.com/gorilla/feeds/doc.go +73 -0
@@ 0,0 1,73 @@
/*
Syndication (feed) generator library for golang.

Installing

	go get github.com/gorilla/feeds

Feeds provides a simple, generic Feed interface with a generic Item object as well as RSS, Atom and JSON Feed specific RssFeed, AtomFeed and JSONFeed objects which allow access to all of each spec's defined elements.

Examples

Create a Feed and some Items in that feed using the generic interfaces:

	import (
		"time"
		. "github.com/gorilla/feeds"
	)

	now = time.Now()

	feed := &Feed{
		Title:       "jmoiron.net blog",
		Link:        &Link{Href: "http://jmoiron.net/blog"},
		Description: "discussion about tech, footie, photos",
		Author:      &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
		Created:     now,
		Copyright:   "This work is copyright © Benjamin Button",
	}

	feed.Items = []*Item{
		&Item{
			Title:       "Limiting Concurrency in Go",
			Link:        &Link{Href: "http://jmoiron.net/blog/limiting-concurrency-in-go/"},
			Description: "A discussion on controlled parallelism in golang",
			Author:      &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
			Created:     now,
		},
		&Item{
			Title:       "Logic-less Template Redux",
			Link:        &Link{Href: "http://jmoiron.net/blog/logicless-template-redux/"},
			Description: "More thoughts on logicless templates",
			Created:     now,
		},
		&Item{
			Title:       "Idiomatic Code Reuse in Go",
			Link:        &Link{Href: "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"},
			Description: "How to use interfaces <em>effectively</em>",
			Created:     now,
		},
	}

From here, you can output Atom, RSS, or JSON Feed versions of this feed easily

	atom, err := feed.ToAtom()
	rss, err := feed.ToRss()
	json, err := feed.ToJSON()

You can also get access to the underlying objects that feeds uses to export its XML

	atomFeed := (&Atom{Feed: feed}).AtomFeed()
	rssFeed := (&Rss{Feed: feed}).RssFeed()
	jsonFeed := (&JSON{Feed: feed}).JSONFeed()

From here, you can modify or add each syndication's specific fields before outputting

	atomFeed.Subtitle = "plays the blues"
	atom, err := ToXML(atomFeed)
	rssFeed.Generator = "gorilla/feeds v1.0 (github.com/gorilla/feeds)"
	rss, err := ToXML(rssFeed)
	jsonFeed.NextUrl = "https://www.example.com/feed.json?page=2"
	json, err := jsonFeed.ToJSON()
*/
package feeds

A vendor/github.com/gorilla/feeds/feed.go => vendor/github.com/gorilla/feeds/feed.go +145 -0
@@ 0,0 1,145 @@
package feeds

import (
	"encoding/json"
	"encoding/xml"
	"io"
	"sort"
	"time"
)

type Link struct {
	Href, Rel, Type, Length string
}

type Author struct {
	Name, Email string
}

type Image struct {
	Url, Title, Link string
	Width, Height    int
}

type Enclosure struct {
	Url, Length, Type string
}

type Item struct {
	Title       string
	Link        *Link
	Source      *Link
	Author      *Author
	Description string // used as description in rss, summary in atom
	Id          string // used as guid in rss, id in atom
	Updated     time.Time
	Created     time.Time
	Enclosure   *Enclosure
	Content     string
}

type Feed struct {
	Title       string
	Link        *Link
	Description string
	Author      *Author
	Updated     time.Time
	Created     time.Time
	Id          string
	Subtitle    string
	Items       []*Item
	Copyright   string
	Image       *Image
}

// add a new Item to a Feed
func (f *Feed) Add(item *Item) {
	f.Items = append(f.Items, item)
}

// returns the first non-zero time formatted as a string or ""
func anyTimeFormat(format string, times ...time.Time) string {
	for _, t := range times {
		if !t.IsZero() {
			return t.Format(format)
		}
	}
	return ""
}

// interface used by ToXML to get a object suitable for exporting XML.
type XmlFeed interface {
	FeedXml() interface{}
}

// turn a feed object (either a Feed, AtomFeed, or RssFeed) into xml
// returns an error if xml marshaling fails
func ToXML(feed XmlFeed) (string, error) {
	x := feed.FeedXml()
	data, err := xml.MarshalIndent(x, "", "  ")
	if err != nil {
		return "", err
	}
	// strip empty line from default xml header
	s := xml.Header[:len(xml.Header)-1] + string(data)
	return s, nil
}

// WriteXML writes a feed object (either a Feed, AtomFeed, or RssFeed) as XML into
// the writer. Returns an error if XML marshaling fails.
func WriteXML(feed XmlFeed, w io.Writer) error {
	x := feed.FeedXml()
	// write default xml header, without the newline
	if _, err := w.Write([]byte(xml.Header[:len(xml.Header)-1])); err != nil {
		return err
	}
	e := xml.NewEncoder(w)
	e.Indent("", "  ")
	return e.Encode(x)
}

// creates an Atom representation of this feed
func (f *Feed) ToAtom() (string, error) {
	a := &Atom{f}
	return ToXML(a)
}

// WriteAtom writes an Atom representation of this feed to the writer.
func (f *Feed) WriteAtom(w io.Writer) error {
	return WriteXML(&Atom{f}, w)
}

// creates an Rss representation of this feed
func (f *Feed) ToRss() (string, error) {
	r := &Rss{f}
	return ToXML(r)
}

// WriteRss writes an RSS representation of this feed to the writer.
func (f *Feed) WriteRss(w io.Writer) error {
	return WriteXML(&Rss{f}, w)
}

// ToJSON creates a JSON Feed representation of this feed
func (f *Feed) ToJSON() (string, error) {
	j := &JSON{f}
	return j.ToJSON()
}

// WriteJSON writes an JSON representation of this feed to the writer.
func (f *Feed) WriteJSON(w io.Writer) error {
	j := &JSON{f}
	feed := j.JSONFeed()

	e := json.NewEncoder(w)
	e.SetIndent("", "  ")
	return e.Encode(feed)
}

// Sort sorts the Items in the feed with the given less function.
func (f *Feed) Sort(less func(a, b *Item) bool) {
	lessFunc := func(i, j int) bool {
		return less(f.Items[i], f.Items[j])
	}
	sort.SliceStable(f.Items, lessFunc)
}

A vendor/github.com/gorilla/feeds/json.go => vendor/github.com/gorilla/feeds/json.go +183 -0
@@ 0,0 1,183 @@
package feeds

import (
	"encoding/json"
	"strings"
	"time"
)

const jsonFeedVersion = "https://jsonfeed.org/version/1"

// JSONAuthor represents the author of the feed or of an individual item
// in the feed
type JSONAuthor struct {
	Name   string `json:"name,omitempty"`
	Url    string `json:"url,omitempty"`
	Avatar string `json:"avatar,omitempty"`
}

// JSONAttachment represents a related resource. Podcasts, for instance, would
// include an attachment that’s an audio or video file.
type JSONAttachment struct {
	Url      string        `json:"url,omitempty"`
	MIMEType string        `json:"mime_type,omitempty"`
	Title    string        `json:"title,omitempty"`
	Size     int32         `json:"size,omitempty"`
	Duration time.Duration `json:"duration_in_seconds,omitempty"`
}

// MarshalJSON implements the json.Marshaler interface.
// The Duration field is marshaled in seconds, all other fields are marshaled
// based upon the definitions in struct tags.
func (a *JSONAttachment) MarshalJSON() ([]byte, error) {
	type EmbeddedJSONAttachment JSONAttachment
	return json.Marshal(&struct {
		Duration float64 `json:"duration_in_seconds,omitempty"`
		*EmbeddedJSONAttachment
	}{
		EmbeddedJSONAttachment: (*EmbeddedJSONAttachment)(a),
		Duration:               a.Duration.Seconds(),
	})
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// The Duration field is expected to be in seconds, all other field types
// match the struct definition.
func (a *JSONAttachment) UnmarshalJSON(data []byte) error {
	type EmbeddedJSONAttachment JSONAttachment
	var raw struct {
		Duration float64 `json:"duration_in_seconds,omitempty"`
		*EmbeddedJSONAttachment
	}
	raw.EmbeddedJSONAttachment = (*EmbeddedJSONAttachment)(a)

	err := json.Unmarshal(data, &raw)
	if err != nil {
		return err
	}

	if raw.Duration > 0 {
		nsec := int64(raw.Duration * float64(time.Second))
		raw.EmbeddedJSONAttachment.Duration = time.Duration(nsec)
	}

	return nil
}

// JSONItem represents a single entry/post for the feed.
type JSONItem struct {
	Id            string           `json:"id"`
	Url           string           `json:"url,omitempty"`
	ExternalUrl   string           `json:"external_url,omitempty"`
	Title         string           `json:"title,omitempty"`
	ContentHTML   string           `json:"content_html,omitempty"`
	ContentText   string           `json:"content_text,omitempty"`
	Summary       string           `json:"summary,omitempty"`
	Image         string           `json:"image,omitempty"`
	BannerImage   string           `json:"banner_,omitempty"`
	PublishedDate *time.Time       `json:"date_published,omitempty"`
	ModifiedDate  *time.Time       `json:"date_modified,omitempty"`
	Author        *JSONAuthor      `json:"author,omitempty"`
	Tags          []string         `json:"tags,omitempty"`
	Attachments   []JSONAttachment `json:"attachments,omitempty"`
}

// JSONHub describes an endpoint that can be used to subscribe to real-time
// notifications from the publisher of this feed.
type JSONHub struct {
	Type string `json:"type"`
	Url  string `json:"url"`
}

// JSONFeed represents a syndication feed in the JSON Feed Version 1 format.
// Matching the specification found here: https://jsonfeed.org/version/1.
type JSONFeed 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"`
	UserComment string      `json:"user_comment,omitempty"`
	NextUrl     string      `json:"next_url,omitempty"`
	Icon        string      `json:"icon,omitempty"`
	Favicon     string      `json:"favicon,omitempty"`
	Author      *JSONAuthor `json:"author,omitempty"`
	Expired     *bool       `json:"expired,omitempty"`
	Hubs        []*JSONItem `json:"hubs,omitempty"`
	Items       []*JSONItem `json:"items,omitempty"`
}

// JSON is used to convert a generic Feed to a JSONFeed.
type JSON struct {
	*Feed
}

// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
func (f *JSON) ToJSON() (string, error) {
	return f.JSONFeed().ToJSON()
}

// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
func (f *JSONFeed) ToJSON() (string, error) {
	data, err := json.MarshalIndent(f, "", "  ")
	if err != nil {
		return "", err
	}

	return string(data), nil
}

// JSONFeed creates a new JSONFeed with a generic Feed struct's data.
func (f *JSON) JSONFeed() *JSONFeed {
	feed := &JSONFeed{
		Version:     jsonFeedVersion,
		Title:       f.Title,
		Description: f.Description,
	}

	if f.Link != nil {
		feed.HomePageUrl = f.Link.Href
	}
	if f.Author != nil {
		feed.Author = &JSONAuthor{
			Name: f.Author.Name,
		}
	}
	for _, e := range f.Items {
		feed.Items = append(feed.Items, newJSONItem(e))
	}
	return feed
}

func newJSONItem(i *Item) *JSONItem {
	item := &JSONItem{
		Id:      i.Id,
		Title:   i.Title,
		Summary: i.Description,

		ContentHTML: i.Content,
	}

	if i.Link != nil {
		item.Url = i.Link.Href
	}
	if i.Source != nil {
		item.ExternalUrl = i.Source.Href
	}
	if i.Author != nil {
		item.Author = &JSONAuthor{
			Name: i.Author.Name,
		}
	}
	if !i.Created.IsZero() {
		item.PublishedDate = &i.Created
	}
	if !i.Updated.IsZero() {
		item.ModifiedDate = &i.Updated
	}
	if i.Enclosure != nil && strings.HasPrefix(i.Enclosure.Type, "image/") {
		item.Image = i.Enclosure.Url
	}

	return item
}

A vendor/github.com/gorilla/feeds/rss.go => vendor/github.com/gorilla/feeds/rss.go +168 -0
@@ 0,0 1,168 @@
package feeds

// rss support
// validation done according to spec here:
//    http://cyber.law.harvard.edu/rss/rss.html

import (
	"encoding/xml"
	"fmt"
	"time"
)

// private wrapper around the RssFeed which gives us the <rss>..</rss> xml
type RssFeedXml struct {
	XMLName          xml.Name `xml:"rss"`
	Version          string   `xml:"version,attr"`
	ContentNamespace string   `xml:"xmlns:content,attr"`
	Channel          *RssFeed
}

type RssContent struct {
	XMLName xml.Name `xml:"content:encoded"`
	Content string   `xml:",cdata"`
}

type RssImage struct {
	XMLName xml.Name `xml:"image"`
	Url     string   `xml:"url"`
	Title   string   `xml:"title"`
	Link    string   `xml:"link"`
	Width   int      `xml:"width,omitempty"`
	Height  int      `xml:"height,omitempty"`
}

type RssTextInput struct {
	XMLName     xml.Name `xml:"textInput"`
	Title       string   `xml:"title"`
	Description string   `xml:"description"`
	Name        string   `xml:"name"`
	Link        string   `xml:"link"`
}

type RssFeed struct {
	XMLName        xml.Name `xml:"channel"`
	Title          string   `xml:"title"`       // required
	Link           string   `xml:"link"`        // required
	Description    string   `xml:"description"` // required
	Language       string   `xml:"language,omitempty"`
	Copyright      string   `xml:"copyright,omitempty"`
	ManagingEditor string   `xml:"managingEditor,omitempty"` // Author used
	WebMaster      string   `xml:"webMaster,omitempty"`
	PubDate        string   `xml:"pubDate,omitempty"`       // created or updated
	LastBuildDate  string   `xml:"lastBuildDate,omitempty"` // updated used
	Category       string   `xml:"category,omitempty"`
	Generator      string   `xml:"generator,omitempty"`
	Docs           string   `xml:"docs,omitempty"`
	Cloud          string   `xml:"cloud,omitempty"`
	Ttl            int      `xml:"ttl,omitempty"`
	Rating         string   `xml:"rating,omitempty"`
	SkipHours      string   `xml:"skipHours,omitempty"`
	SkipDays       string   `xml:"skipDays,omitempty"`
	Image          *RssImage
	TextInput      *RssTextInput
	Items          []*RssItem `xml:"item"`
}

type RssItem struct {
	XMLName     xml.Name `xml:"item"`
	Title       string   `xml:"title"`       // required
	Link        string   `xml:"link"`        // required
	Description string   `xml:"description"` // required
	Content     *RssContent
	Author      string `xml:"author,omitempty"`
	Category    string `xml:"category,omitempty"`
	Comments    string `xml:"comments,omitempty"`
	Enclosure   *RssEnclosure
	Guid        string `xml:"guid,omitempty"`    // Id used
	PubDate     string `xml:"pubDate,omitempty"` // created or updated
	Source      string `xml:"source,omitempty"`
}

type RssEnclosure struct {
	//RSS 2.0 <enclosure url="http://example.com/file.mp3" length="123456789" type="audio/mpeg" />
	XMLName xml.Name `xml:"enclosure"`
	Url     string   `xml:"url,attr"`
	Length  string   `xml:"length,attr"`
	Type    string   `xml:"type,attr"`
}

type Rss struct {
	*Feed
}

// create a new RssItem with a generic Item struct's data
func newRssItem(i *Item) *RssItem {
	item := &RssItem{
		Title:       i.Title,
		Link:        i.Link.Href,
		Description: i.Description,
		Guid:        i.Id,
		PubDate:     anyTimeFormat(time.RFC1123Z, i.Created, i.Updated),
	}
	if len(i.Content) > 0 {
		item.Content = &RssContent{Content: i.Content}
	}
	if i.Source != nil {
		item.Source = i.Source.Href
	}

	// Define a closure
	if i.Enclosure != nil && i.Enclosure.Type != "" && i.Enclosure.Length != "" {
		item.Enclosure = &RssEnclosure{Url: i.Enclosure.Url, Type: i.Enclosure.Type, Length: i.Enclosure.Length}
	}

	if i.Author != nil {
		item.Author = i.Author.Name
	}
	return item
}

// create a new RssFeed with a generic Feed struct's data
func (r *Rss) RssFeed() *RssFeed {
	pub := anyTimeFormat(time.RFC1123Z, r.Created, r.Updated)
	build := anyTimeFormat(time.RFC1123Z, r.Updated)
	author := ""
	if r.Author != nil {
		author = r.Author.Email
		if len(r.Author.Name) > 0 {
			author = fmt.Sprintf("%s (%s)", r.Author.Email, r.Author.Name)
		}
	}

	var image *RssImage
	if r.Image != nil {
		image = &RssImage{Url: r.Image.Url, Title: r.Image.Title, Link: r.Image.Link, Width: r.Image.Width, Height: r.Image.Height}
	}

	channel := &RssFeed{
		Title:          r.Title,
		Link:           r.Link.Href,
		Description:    r.Description,
		ManagingEditor: author,
		PubDate:        pub,
		LastBuildDate:  build,
		Copyright:      r.Copyright,
		Image:          image,
	}
	for _, i := range r.Items {
		channel.Items = append(channel.Items, newRssItem(i))
	}
	return channel
}

// FeedXml returns an XML-Ready object for an Rss object
func (r *Rss) FeedXml() interface{} {
	// only generate version 2.0 feeds for now
	return r.RssFeed().FeedXml()

}

// FeedXml returns an XML-ready object for an RssFeed object
func (r *RssFeed) FeedXml() interface{} {
	return &RssFeedXml{
		Version:          "2.0",
		Channel:          r,
		ContentNamespace: "http://purl.org/rss/1.0/modules/content/",
	}
}

A vendor/github.com/gorilla/feeds/test.atom => vendor/github.com/gorilla/feeds/test.atom +92 -0
@@ 0,0 1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns:atom="http://www.w3.org/2005/Atom">
        <title><![CDATA[Lorem ipsum feed for an interval of 1 minutes]]></title>
        <description><![CDATA[This is a constantly updating lorem ipsum feed]]></description>
        <link>http://example.com/</link>
        <generator>RSS for Node</generator>
        <lastBuildDate>Tue, 30 Oct 2018 23:22:37 GMT</lastBuildDate>
        <author><![CDATA[John Smith]]></author>
        <pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
        <copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright>
        <ttl>60</ttl>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:22:00+00:00]]></title>
            <description><![CDATA[Exercitation ut Lorem sint proident.]]></description>
            <link>http://example.com/test/1540941720</link>
            <guid isPermaLink="true">http://example.com/test/1540941720</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:21:00+00:00]]></title>
            <description><![CDATA[Ea est do quis fugiat exercitation.]]></description>
            <link>http://example.com/test/1540941660</link>
            <guid isPermaLink="true">http://example.com/test/1540941660</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:21:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:20:00+00:00]]></title>
            <description><![CDATA[Ipsum velit cillum ad laborum sit nulla exercitation consequat sint veniam culpa veniam voluptate incididunt.]]></description>
            <link>http://example.com/test/1540941600</link>
            <guid isPermaLink="true">http://example.com/test/1540941600</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:20:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:19:00+00:00]]></title>
            <description><![CDATA[Ullamco pariatur aliqua consequat ea veniam id qui incididunt laborum.]]></description>
            <link>http://example.com/test/1540941540</link>
            <guid isPermaLink="true">http://example.com/test/1540941540</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:19:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:18:00+00:00]]></title>
            <description><![CDATA[Velit proident aliquip aliquip anim mollit voluptate laboris voluptate et occaecat occaecat laboris ea nulla.]]></description>
            <link>http://example.com/test/1540941480</link>
            <guid isPermaLink="true">http://example.com/test/1540941480</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:18:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:17:00+00:00]]></title>
            <description><![CDATA[Do in quis mollit consequat id in minim laborum sint exercitation laborum elit officia.]]></description>
            <link>http://example.com/test/1540941420</link>
            <guid isPermaLink="true">http://example.com/test/1540941420</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:17:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:16:00+00:00]]></title>
            <description><![CDATA[Irure id sint ullamco Lorem magna consectetur officia adipisicing duis incididunt.]]></description>
            <link>http://example.com/test/1540941360</link>
            <guid isPermaLink="true">http://example.com/test/1540941360</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:16:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:15:00+00:00]]></title>
            <description><![CDATA[Sunt anim excepteur esse nisi commodo culpa laborum exercitation ad anim ex elit.]]></description>
            <link>http://example.com/test/1540941300</link>
            <guid isPermaLink="true">http://example.com/test/1540941300</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:15:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:14:00+00:00]]></title>
            <description><![CDATA[Excepteur aliquip fugiat ex labore nisi.]]></description>
            <link>http://example.com/test/1540941240</link>
            <guid isPermaLink="true">http://example.com/test/1540941240</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:14:00 GMT</pubDate>
        </entry>
        <entry>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:13:00+00:00]]></title>
            <description><![CDATA[Id proident adipisicing proident pariatur aute pariatur pariatur dolor dolor in voluptate dolor.]]></description>
            <link>http://example.com/test/1540941180</link>
            <guid isPermaLink="true">http://example.com/test/1540941180</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:13:00 GMT</pubDate>
        </entry>
</feed>
\ No newline at end of file

A vendor/github.com/gorilla/feeds/test.rss => vendor/github.com/gorilla/feeds/test.rss +96 -0
@@ 0,0 1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" 
    xmlns:content="http://purl.org/rss/1.0/modules/content/" 
    xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[Lorem ipsum feed for an interval of 1 minutes]]></title>
        <description><![CDATA[This is a constantly updating lorem ipsum feed]]></description>
        <link>http://example.com/</link>
        <generator>RSS for Node</generator>
        <lastBuildDate>Tue, 30 Oct 2018 23:22:37 GMT</lastBuildDate>
        <author><![CDATA[John Smith]]></author>
        <pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
        <copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright>
        <ttl>60</ttl>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:22:00+00:00]]></title>
            <description><![CDATA[Exercitation ut Lorem sint proident.]]></description>
            <link>http://example.com/test/1540941720</link>
            <guid isPermaLink="true">http://example.com/test/1540941720</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:21:00+00:00]]></title>
            <description><![CDATA[Ea est do quis fugiat exercitation.]]></description>
            <link>http://example.com/test/1540941660</link>
            <guid isPermaLink="true">http://example.com/test/1540941660</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:21:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:20:00+00:00]]></title>
            <description><![CDATA[Ipsum velit cillum ad laborum sit nulla exercitation consequat sint veniam culpa veniam voluptate incididunt.]]></description>
            <link>http://example.com/test/1540941600</link>
            <guid isPermaLink="true">http://example.com/test/1540941600</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:20:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:19:00+00:00]]></title>
            <description><![CDATA[Ullamco pariatur aliqua consequat ea veniam id qui incididunt laborum.]]></description>
            <link>http://example.com/test/1540941540</link>
            <guid isPermaLink="true">http://example.com/test/1540941540</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:19:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:18:00+00:00]]></title>
            <description><![CDATA[Velit proident aliquip aliquip anim mollit voluptate laboris voluptate et occaecat occaecat laboris ea nulla.]]></description>
            <link>http://example.com/test/1540941480</link>
            <guid isPermaLink="true">http://example.com/test/1540941480</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:18:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:17:00+00:00]]></title>
            <description><![CDATA[Do in quis mollit consequat id in minim laborum sint exercitation laborum elit officia.]]></description>
            <link>http://example.com/test/1540941420</link>
            <guid isPermaLink="true">http://example.com/test/1540941420</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:17:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:16:00+00:00]]></title>
            <description><![CDATA[Irure id sint ullamco Lorem magna consectetur officia adipisicing duis incididunt.]]></description>
            <link>http://example.com/test/1540941360</link>
            <guid isPermaLink="true">http://example.com/test/1540941360</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:16:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:15:00+00:00]]></title>
            <description><![CDATA[Sunt anim excepteur esse nisi commodo culpa laborum exercitation ad anim ex elit.]]></description>
            <link>http://example.com/test/1540941300</link>
            <guid isPermaLink="true">http://example.com/test/1540941300</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:15:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:14:00+00:00]]></title>
            <description><![CDATA[Excepteur aliquip fugiat ex labore nisi.]]></description>
            <link>http://example.com/test/1540941240</link>
            <guid isPermaLink="true">http://example.com/test/1540941240</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:14:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Lorem ipsum 2018-10-30T23:13:00+00:00]]></title>
            <description><![CDATA[Id proident adipisicing proident pariatur aute pariatur pariatur dolor dolor in voluptate dolor.]]></description>
            <link>http://example.com/test/1540941180</link>
            <guid isPermaLink="true">http://example.com/test/1540941180</guid>
            <dc:creator><![CDATA[John Smith]]></dc:creator>
            <pubDate>Tue, 30 Oct 2018 23:13:00 GMT</pubDate>
        </item>
    </channel>
</rss>
\ No newline at end of file

A vendor/github.com/gorilla/feeds/to-implement.md => vendor/github.com/gorilla/feeds/to-implement.md +20 -0
@@ 0,0 1,20 @@
[Full iTunes list](https://help.apple.com/itc/podcasts_connect/#/itcb54353390)

[Example of ideal iTunes RSS feed](https://help.apple.com/itc/podcasts_connect/#/itcbaf351599)

```
<itunes:author>
<itunes:block>
<itunes:catergory>
<itunes:image>
<itunes:duration>
<itunes:explicit>
<itunes:isClosedCaptioned>
<itunes:order>
<itunes:complete>
<itunes:new-feed-url>
<itunes:owner>
<itunes:subtitle>
<itunes:summary>
<language>
```
\ No newline at end of file

A vendor/github.com/gorilla/feeds/uuid.go => vendor/github.com/gorilla/feeds/uuid.go +27 -0
@@ 0,0 1,27 @@
package feeds

// relevant bits from https://github.com/abneptis/GoUUID/blob/master/uuid.go

import (
	"crypto/rand"
	"fmt"
)

type UUID [16]byte

// create a new uuid v4
func NewUUID() *UUID {
	u := &UUID{}
	_, err := rand.Read(u[:16])
	if err != nil {
		panic(err)
	}

	u[8] = (u[8] | 0x80) & 0xBf
	u[6] = (u[6] | 0x40) & 0x4f
	return u
}

func (u *UUID) String() string {
	return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:])
}

M vendor/modules.txt => vendor/modules.txt +3 -0
@@ 1,5 1,8 @@
# git.sr.ht/~evanj/embed v0.0.0-20200525225021-2cde7dae7bfa
## explicit
# github.com/gorilla/feeds v1.1.1
## explicit
github.com/gorilla/feeds
# github.com/tdewolff/minify/v2 v2.7.3
## explicit
github.com/tdewolff/minify/v2