opml package

This commit is contained in:
Nazar Kanaev 2021-03-16 23:25:51 +00:00
parent 0d49377879
commit 1e65da9aa4
4 changed files with 148 additions and 58 deletions

83
src/opml/builder.go Normal file
View File

@ -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) + `<outline type="rss" text="%s" description="%s" xmlUrl="%s" htmlUrl="%s"/>`,
feed.title, feed.description,
feed.feedUrl, feed.siteUrl,
)
}
line(`<?xml version="1.0" encoding="UTF-8"?>`)
line(`<opml version="1.1">`)
line(`<head><title>Subscriptions</title></head>`)
line(`<body>`)
for _, folder := range b.folders {
line(` <outline text="%s">`, folder.title)
for _, feed := range folder.feeds {
feedline(feed, 4)
}
line(` </outline>`)
}
for _, feed := range b.rootfolder.feeds {
feedline(feed, 2)
}
line(`</body>`)
line(`</opml>`)
return builder.String()
}

30
src/opml/builder_test.go Normal file
View File

@ -0,0 +1,30 @@
package opml
import "testing"
var sample = `<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.1">
<head><title>Subscriptions</title></head>
<body>
<outline text="sub">
<outline type="rss" text="subtitle1" description="sub1" xmlUrl="https://foo.com/feed.xml" htmlUrl="https://foo.com/"/>
<outline type="rss" text="&amp;&gt;" description="&lt;&gt;" xmlUrl="https://bar.com/feed.xml" htmlUrl="https://bar.com/"/>
</outline>
<outline type="rss" text="title1" description="desc1" xmlUrl="https://example.com/feed.xml" htmlUrl="https://example.com/"/>
</body>
</opml>
`
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)
}
}

View File

@ -1,27 +1,27 @@
package server package opml
import ( import (
"encoding/xml" "encoding/xml"
"io" "io"
) )
type opml struct { type OPML struct {
XMLName xml.Name `xml:"opml"` XMLName xml.Name `xml:"opml"`
Version string `xml:"version,attr"` 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"` Type string `xml:"type,attr,omitempty"`
Title string `xml:"text,attr"` Title string `xml:"text,attr"`
FeedURL string `xml:"xmlUrl,attr,omitempty"` FeedURL string `xml:"xmlUrl,attr,omitempty"`
SiteURL string `xml:"htmlUrl,attr,omitempty"` SiteURL string `xml:"htmlUrl,attr,omitempty"`
Description string `xml:"description,attr,omitempty"` Description string `xml:"description,attr,omitempty"`
Outlines []outline `xml:"outline,omitempty"` Outlines []Outline `xml:"outline,omitempty"`
} }
func (o outline) AllFeeds() []outline { func (o Outline) AllFeeds() []Outline {
result := make([]outline, 0) result := make([]Outline, 0)
for _, sub := range o.Outlines { for _, sub := range o.Outlines {
if sub.Type == "rss" { if sub.Type == "rss" {
result = append(result, sub) result = append(result, sub)
@ -32,8 +32,8 @@ func (o outline) AllFeeds() []outline {
return result return result
} }
func parseOPML(r io.Reader) (*opml, error) { func Parse(r io.Reader) (*OPML, error) {
feeds := new(opml) feeds := new(OPML)
decoder := xml.NewDecoder(r) decoder := xml.NewDecoder(r)
decoder.Entity = xml.HTMLEntity decoder.Entity = xml.HTMLEntity
decoder.Strict = false decoder.Strict = false

View File

@ -2,19 +2,17 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/nkanaev/yarr/src/assets" "github.com/nkanaev/yarr/src/assets"
"github.com/nkanaev/yarr/src/auth" "github.com/nkanaev/yarr/src/auth"
"github.com/nkanaev/yarr/src/router" "github.com/nkanaev/yarr/src/router"
"github.com/nkanaev/yarr/src/storage" "github.com/nkanaev/yarr/src/storage"
"html" "github.com/nkanaev/yarr/src/opml"
"io/ioutil" "io/ioutil"
"log" "log"
"math" "math"
"net/http" "net/http"
"reflect" "reflect"
"strconv" "strconv"
"strings"
) )
func (s *Server) handler() http.Handler { func (s *Server) handler() http.Handler {
@ -338,7 +336,7 @@ func (s *Server) handleOPMLImport(c *router.Context) {
log.Print(err) log.Print(err)
return return
} }
doc, err := parseOPML(file) doc, err := opml.Parse(file)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return 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-Type", "application/xml; charset=utf-8")
c.Out.Header().Set("Content-Disposition", `attachment; filename="subscriptions.opml"`) c.Out.Header().Set("Content-Disposition", `attachment; filename="subscriptions.opml"`)
builder := strings.Builder{} rootFeeds := make([]*storage.Feed, 0)
feedsByFolderID := make(map[int64][]*storage.Feed)
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)+
`<outline type="rss" text="%s" description="%s" xmlUrl="%s" htmlUrl="%s"/>`,
feed.Title, feed.Description,
feed.FeedLink, feed.Link,
)
}
line(`<?xml version="1.0" encoding="UTF-8"?>`)
line(`<opml version="1.1">`)
line(`<head>`)
line(` <title>subscriptions.opml</title>`)
line(`</head>`)
line(`<body>`)
feedsByFolderID := make(map[int64][]storage.Feed)
for _, feed := range s.db.ListFeeds() { for _, feed := range s.db.ListFeeds() {
var folderId = int64(0) feed := feed
if feed.FolderId != nil { if feed.FolderId == nil {
folderId = *feed.FolderId rootFeeds = append(rootFeeds, &feed)
} else {
id := *feed.FolderId
if feedsByFolderID[id] == nil {
feedsByFolderID[id] = make([]*storage.Feed, 0)
} }
if feedsByFolderID[folderId] == nil { feedsByFolderID[id] = append(feedsByFolderID[id], &feed)
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() { for _, folder := range s.db.ListFolders() {
line(` <outline text="%s">`, folder.Title) folderFeeds := feedsByFolderID[folder.Id]
for _, feed := range feedsByFolderID[folder.Id] { if len(folderFeeds) == 0 {
feedline(feed, 4) continue
} }
line(` </outline>`) feedFolder := builder.AddFolder(folder.Title)
for _, feed := range folderFeeds {
feedFolder.AddFeed(feed.Title, feed.Description, feed.FeedLink, feed.Link)
} }
for _, feed := range feedsByFolderID[0] {
feedline(feed, 2)
} }
line(`</body>`)
line(`</opml>`)
c.Out.Write([]byte(builder.String())) c.Out.Write([]byte(builder.String()))
} }
} }