diff --git a/src/opml/builder.go b/src/opml/builder.go
new file mode 100644
index 0000000..2e0c95c
--- /dev/null
+++ b/src/opml/builder.go
@@ -0,0 +1,83 @@
+package opml
+
+import (
+ "fmt"
+ "html"
+ "strings"
+)
+
+type OPMLBuilder struct {
+ rootfolder *OPMLFolder
+ folders []*OPMLFolder
+}
+
+type OPMLFolder struct {
+ title string
+ feeds []*OPMLFeed
+}
+
+type OPMLFeed struct {
+ title, description, feedUrl, siteUrl string
+}
+
+func NewBuilder() *OPMLBuilder {
+ return &OPMLBuilder{
+ rootfolder: &OPMLFolder{feeds: make([]*OPMLFeed, 0)},
+ folders: make([]*OPMLFolder, 0),
+ }
+}
+
+func (b *OPMLBuilder) AddFeed(title, description, feedUrl, siteUrl string) {
+ b.rootfolder.AddFeed(title, description, feedUrl, siteUrl)
+}
+
+func (b *OPMLBuilder) AddFolder(title string) *OPMLFolder {
+ folder := &OPMLFolder{title: title}
+ b.folders = append(b.folders, folder)
+ return folder
+}
+
+func (f *OPMLFolder) AddFeed(title, description, feedUrl, siteUrl string) {
+ f.feeds = append(f.feeds, &OPMLFeed{title, description, feedUrl, siteUrl})
+}
+
+func (b *OPMLBuilder) String() string {
+ builder := strings.Builder{}
+
+ line := func(s string, args ...string) {
+ if len(args) > 0 {
+ escapedargs := make([]interface{}, len(args))
+ for idx, arg := range args {
+ escapedargs[idx] = html.EscapeString(arg)
+ }
+ s = fmt.Sprintf(s, escapedargs...)
+ }
+ builder.WriteString(s)
+ builder.WriteRune('\n')
+ }
+ feedline := func(feed *OPMLFeed, indent int) {
+ line(
+ strings.Repeat(" ", indent) + ``,
+ feed.title, feed.description,
+ feed.feedUrl, feed.siteUrl,
+ )
+ }
+ line(``)
+ line(``)
+ line(`Subscriptions`)
+ line(``)
+ for _, folder := range b.folders {
+ line(` `, folder.title)
+ for _, feed := range folder.feeds {
+ feedline(feed, 4)
+ }
+ line(` `)
+ }
+ for _, feed := range b.rootfolder.feeds {
+ feedline(feed, 2)
+ }
+ line(``)
+ line(``)
+
+ return builder.String()
+}
diff --git a/src/opml/builder_test.go b/src/opml/builder_test.go
new file mode 100644
index 0000000..9868874
--- /dev/null
+++ b/src/opml/builder_test.go
@@ -0,0 +1,30 @@
+package opml
+
+import "testing"
+
+var sample = `
+
+Subscriptions
+
+
+
+
+
+
+
+
+`
+
+func TestOPMLBuilder(t *testing.T) {
+ builder := NewBuilder()
+ builder.AddFeed("title1", "desc1", "https://example.com/feed.xml", "https://example.com/")
+
+ folder := builder.AddFolder("sub")
+ folder.AddFeed("subtitle1", "sub1", "https://foo.com/feed.xml", "https://foo.com/")
+ folder.AddFeed("&>", "<>", "https://bar.com/feed.xml", "https://bar.com/")
+
+ output := builder.String()
+ if output != sample {
+ t.Errorf("\n=== expected:\n%s\n=== got:\n%s\n===", sample, output)
+ }
+}
diff --git a/src/server/opml.go b/src/opml/parser.go
similarity index 70%
rename from src/server/opml.go
rename to src/opml/parser.go
index aa39cfe..377cf1c 100644
--- a/src/server/opml.go
+++ b/src/opml/parser.go
@@ -1,27 +1,27 @@
-package server
+package opml
import (
"encoding/xml"
"io"
)
-type opml struct {
+type OPML struct {
XMLName xml.Name `xml:"opml"`
Version string `xml:"version,attr"`
- Outlines []outline `xml:"body>outline"`
+ Outlines []Outline `xml:"body>outline"`
}
-type outline struct {
+type Outline struct {
Type string `xml:"type,attr,omitempty"`
Title string `xml:"text,attr"`
FeedURL string `xml:"xmlUrl,attr,omitempty"`
SiteURL string `xml:"htmlUrl,attr,omitempty"`
Description string `xml:"description,attr,omitempty"`
- Outlines []outline `xml:"outline,omitempty"`
+ Outlines []Outline `xml:"outline,omitempty"`
}
-func (o outline) AllFeeds() []outline {
- result := make([]outline, 0)
+func (o Outline) AllFeeds() []Outline {
+ result := make([]Outline, 0)
for _, sub := range o.Outlines {
if sub.Type == "rss" {
result = append(result, sub)
@@ -32,8 +32,8 @@ func (o outline) AllFeeds() []outline {
return result
}
-func parseOPML(r io.Reader) (*opml, error) {
- feeds := new(opml)
+func Parse(r io.Reader) (*OPML, error) {
+ feeds := new(OPML)
decoder := xml.NewDecoder(r)
decoder.Entity = xml.HTMLEntity
decoder.Strict = false
diff --git a/src/server/handlers.go b/src/server/handlers.go
index 3417c2c..ca2e0e8 100644
--- a/src/server/handlers.go
+++ b/src/server/handlers.go
@@ -2,19 +2,17 @@ package server
import (
"encoding/json"
- "fmt"
"github.com/nkanaev/yarr/src/assets"
"github.com/nkanaev/yarr/src/auth"
"github.com/nkanaev/yarr/src/router"
"github.com/nkanaev/yarr/src/storage"
- "html"
+ "github.com/nkanaev/yarr/src/opml"
"io/ioutil"
"log"
"math"
"net/http"
"reflect"
"strconv"
- "strings"
)
func (s *Server) handler() http.Handler {
@@ -338,7 +336,7 @@ func (s *Server) handleOPMLImport(c *router.Context) {
log.Print(err)
return
}
- doc, err := parseOPML(file)
+ doc, err := opml.Parse(file)
if err != nil {
log.Print(err)
return
@@ -365,57 +363,36 @@ func (s *Server) handleOPMLExport(c *router.Context) {
c.Out.Header().Set("Content-Type", "application/xml; charset=utf-8")
c.Out.Header().Set("Content-Disposition", `attachment; filename="subscriptions.opml"`)
- builder := strings.Builder{}
-
- line := func(s string, args ...string) {
- if len(args) > 0 {
- escapedargs := make([]interface{}, len(args))
- for idx, arg := range args {
- escapedargs[idx] = html.EscapeString(arg)
- }
- s = fmt.Sprintf(s, escapedargs...)
- }
- builder.WriteString(s)
- builder.WriteString("\n")
- }
-
- feedline := func(feed storage.Feed, indent int) {
- line(
- strings.Repeat(" ", indent)+
- ``,
- feed.Title, feed.Description,
- feed.FeedLink, feed.Link,
- )
- }
- line(``)
- line(``)
- line(``)
- line(` subscriptions.opml`)
- line(``)
- line(``)
- feedsByFolderID := make(map[int64][]storage.Feed)
+ rootFeeds := make([]*storage.Feed, 0)
+ feedsByFolderID := make(map[int64][]*storage.Feed)
for _, feed := range s.db.ListFeeds() {
- var folderId = int64(0)
- if feed.FolderId != nil {
- folderId = *feed.FolderId
+ feed := feed
+ if feed.FolderId == nil {
+ rootFeeds = append(rootFeeds, &feed)
+ } else {
+ id := *feed.FolderId
+ if feedsByFolderID[id] == nil {
+ feedsByFolderID[id] = make([]*storage.Feed, 0)
+ }
+ feedsByFolderID[id] = append(feedsByFolderID[id], &feed)
}
- if feedsByFolderID[folderId] == nil {
- feedsByFolderID[folderId] = make([]storage.Feed, 0)
- }
- feedsByFolderID[folderId] = append(feedsByFolderID[folderId], feed)
+ }
+ builder := opml.NewBuilder()
+
+ for _, feed := range rootFeeds {
+ builder.AddFeed(feed.Title, feed.Description, feed.FeedLink, feed.Link)
}
for _, folder := range s.db.ListFolders() {
- line(` `, folder.Title)
- for _, feed := range feedsByFolderID[folder.Id] {
- feedline(feed, 4)
+ folderFeeds := feedsByFolderID[folder.Id]
+ if len(folderFeeds) == 0 {
+ continue
+ }
+ feedFolder := builder.AddFolder(folder.Title)
+ for _, feed := range folderFeeds {
+ feedFolder.AddFeed(feed.Title, feed.Description, feed.FeedLink, feed.Link)
}
- line(` `)
}
- for _, feed := range feedsByFolderID[0] {
- feedline(feed, 2)
- }
- line(``)
- line(``)
+
c.Out.Write([]byte(builder.String()))
}
}