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())) } }