mirror of
https://github.com/nkanaev/yarr.git
synced 2025-05-24 00:33:14 +00:00
rewrite opml
This commit is contained in:
parent
1c810f68f8
commit
7d7feda319
@ -1,83 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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="&>" description="<>" 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)
|
|
||||||
}
|
|
||||||
}
|
|
78
src/opml/opml.go
Normal file
78
src/opml/opml.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package opml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"html"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Folder struct {
|
||||||
|
Title string
|
||||||
|
Folders []*Folder
|
||||||
|
Feeds []*Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
Title string
|
||||||
|
FeedUrl string
|
||||||
|
SiteUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFolder(title string) *Folder {
|
||||||
|
return &Folder{
|
||||||
|
Title: title,
|
||||||
|
Folders: make([]*Folder, 0),
|
||||||
|
Feeds: make([]*Feed, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Folder) AllFeeds() []*Feed {
|
||||||
|
feeds := make([]*Feed, 0)
|
||||||
|
feeds = append(feeds, f.Feeds...)
|
||||||
|
for _, subfolder := range f.Folders {
|
||||||
|
feeds = append(feeds, subfolder.AllFeeds()...)
|
||||||
|
}
|
||||||
|
return feeds
|
||||||
|
}
|
||||||
|
|
||||||
|
var e = html.EscapeString
|
||||||
|
var indent = " "
|
||||||
|
var nl = "\n"
|
||||||
|
|
||||||
|
func (f *Folder) outline(level int) string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
prefix := strings.Repeat(indent, level)
|
||||||
|
|
||||||
|
if level > 0 {
|
||||||
|
builder.WriteString(prefix + fmt.Sprintf(`<outline text="%s">` + nl, e(f.Title)))
|
||||||
|
}
|
||||||
|
for _, folder := range f.Folders {
|
||||||
|
builder.WriteString(folder.outline(level + 1))
|
||||||
|
}
|
||||||
|
for _, feed := range f.Feeds {
|
||||||
|
builder.WriteString(feed.outline(level + 1))
|
||||||
|
}
|
||||||
|
if level > 0 {
|
||||||
|
builder.WriteString(prefix + `</outline>` + nl)
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Feed) outline(level int) string {
|
||||||
|
return strings.Repeat(indent, level) + fmt.Sprintf(
|
||||||
|
`<outline type="rss" text="%s" xmlUrl="%s" htmlUrl="%s"/>` + nl,
|
||||||
|
e(f.Title), e(f.FeedUrl), e(f.SiteUrl),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Folder) OPML() string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
builder.WriteString(`<?xml version="1.0" encoding="UTF-8"?>` + nl)
|
||||||
|
builder.WriteString(`<opml version="1.1">` + nl)
|
||||||
|
builder.WriteString(`<head><title>subscriptions</title></head>` + nl)
|
||||||
|
builder.WriteString(`<body>` + nl)
|
||||||
|
builder.WriteString(f.outline(0))
|
||||||
|
builder.WriteString(`</body>` + nl)
|
||||||
|
builder.WriteString(`</opml>` + nl)
|
||||||
|
return builder.String()
|
||||||
|
}
|
57
src/opml/opml_test.go
Normal file
57
src/opml/opml_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package opml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func TestOPML(t *testing.T) {
|
||||||
|
have := (&Folder{
|
||||||
|
Title: "",
|
||||||
|
Feeds: []*Feed{
|
||||||
|
&Feed{
|
||||||
|
Title: "title1",
|
||||||
|
FeedUrl: "https://baz.com/feed.xml",
|
||||||
|
SiteUrl: "https://baz.com/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Folders: []*Folder{
|
||||||
|
&Folder{
|
||||||
|
Title: "sub",
|
||||||
|
Feeds: []*Feed{
|
||||||
|
&Feed{
|
||||||
|
Title: "subtitle1",
|
||||||
|
FeedUrl: "https://foo.com/feed.xml",
|
||||||
|
SiteUrl: "https://foo.com/",
|
||||||
|
},
|
||||||
|
&Feed{
|
||||||
|
Title: "&>",
|
||||||
|
FeedUrl: "https://bar.com/feed.xml",
|
||||||
|
SiteUrl: "https://bar.com/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Folders: []*Folder{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).OPML()
|
||||||
|
want := `<?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" xmlUrl="https://foo.com/feed.xml" htmlUrl="https://foo.com/"/>
|
||||||
|
<outline type="rss" text="&>" xmlUrl="https://bar.com/feed.xml" htmlUrl="https://bar.com/"/>
|
||||||
|
</outline>
|
||||||
|
<outline type="rss" text="title1" xmlUrl="https://baz.com/feed.xml" htmlUrl="https://baz.com/"/>
|
||||||
|
</body>
|
||||||
|
</opml>
|
||||||
|
`
|
||||||
|
fmt.Println(have)
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %s", want)
|
||||||
|
t.Logf("have: %s", have)
|
||||||
|
t.Fatal("invalid opml")
|
||||||
|
}
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
package opml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OPML struct {
|
|
||||||
XMLName xml.Name `xml:"opml"`
|
|
||||||
Version string `xml:"version,attr"`
|
|
||||||
Outlines []Outline `xml:"body>outline"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Outline) AllFeeds() []Outline {
|
|
||||||
result := make([]Outline, 0)
|
|
||||||
for _, sub := range o.Outlines {
|
|
||||||
if sub.Type == "rss" {
|
|
||||||
result = append(result, sub)
|
|
||||||
} else {
|
|
||||||
result = append(result, sub.AllFeeds()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func Parse(r io.Reader) (*OPML, error) {
|
|
||||||
feeds := new(OPML)
|
|
||||||
decoder := xml.NewDecoder(r)
|
|
||||||
decoder.Entity = xml.HTMLEntity
|
|
||||||
decoder.Strict = false
|
|
||||||
err := decoder.Decode(&feeds)
|
|
||||||
return feeds, err
|
|
||||||
}
|
|
49
src/opml/read.go
Normal file
49
src/opml/read.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package opml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type opml struct {
|
||||||
|
XMLName xml.Name `xml:"opml"`
|
||||||
|
Outlines []outline `xml:"body>outline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Outlines []outline `xml:"outline,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFolder(title string, outlines []outline) *Folder {
|
||||||
|
folder := NewFolder(title)
|
||||||
|
for _, outline := range outlines {
|
||||||
|
if outline.Type == "rss" {
|
||||||
|
folder.Feeds = append(folder.Feeds, &Feed{
|
||||||
|
Title: outline.Title,
|
||||||
|
FeedUrl: outline.FeedUrl,
|
||||||
|
SiteUrl: outline.SiteUrl,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
subfolder := buildFolder(outline.Title, outline.Outlines)
|
||||||
|
folder.Folders = append(folder.Folders, subfolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(r io.Reader) (*Folder, error) {
|
||||||
|
val := new(opml)
|
||||||
|
decoder := xml.NewDecoder(r)
|
||||||
|
decoder.Entity = xml.HTMLEntity
|
||||||
|
decoder.Strict = false
|
||||||
|
|
||||||
|
err := decoder.Decode(&val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buildFolder("", val.Outlines), nil
|
||||||
|
}
|
60
src/opml/read_test.go
Normal file
60
src/opml/read_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package opml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
have, _ := Parse(strings.NewReader(`
|
||||||
|
<?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="&>" description="<>"
|
||||||
|
xmlUrl="https://bar.com/feed.xml" htmlUrl="https://bar.com/"/>
|
||||||
|
</outline>
|
||||||
|
<outline type="rss" text="title1" description="desc1"
|
||||||
|
xmlUrl="https://baz.com/feed.xml" htmlUrl="https://baz.com/"/>
|
||||||
|
</body>
|
||||||
|
</opml>
|
||||||
|
`))
|
||||||
|
want := &Folder{
|
||||||
|
Title: "",
|
||||||
|
Feeds: []*Feed{
|
||||||
|
&Feed{
|
||||||
|
Title: "title1",
|
||||||
|
FeedUrl: "https://baz.com/feed.xml",
|
||||||
|
SiteUrl: "https://baz.com/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Folders: []*Folder{
|
||||||
|
&Folder{
|
||||||
|
Title: "sub",
|
||||||
|
Feeds: []*Feed{
|
||||||
|
&Feed{
|
||||||
|
Title: "subtitle1",
|
||||||
|
FeedUrl: "https://foo.com/feed.xml",
|
||||||
|
SiteUrl: "https://foo.com/",
|
||||||
|
},
|
||||||
|
&Feed{
|
||||||
|
Title: "&>",
|
||||||
|
FeedUrl: "https://bar.com/feed.xml",
|
||||||
|
SiteUrl: "https://bar.com/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Folders: []*Folder{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.Fatal("invalid opml")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user