diff --git a/src/opml/builder.go b/src/opml/builder.go deleted file mode 100644 index 2e0c95c..0000000 --- a/src/opml/builder.go +++ /dev/null @@ -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) + ``, - 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 deleted file mode 100644 index 9868874..0000000 --- a/src/opml/builder_test.go +++ /dev/null @@ -1,30 +0,0 @@ -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/opml/opml.go b/src/opml/opml.go new file mode 100644 index 0000000..a77f220 --- /dev/null +++ b/src/opml/opml.go @@ -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(`` + 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 + `` + nl) + } + return builder.String() +} + +func (f *Feed) outline(level int) string { + return strings.Repeat(indent, level) + fmt.Sprintf( + `` + nl, + e(f.Title), e(f.FeedUrl), e(f.SiteUrl), + ) +} + +func (f *Folder) OPML() string { + builder := strings.Builder{} + builder.WriteString(`` + nl) + builder.WriteString(`` + nl) + builder.WriteString(`subscriptions` + nl) + builder.WriteString(`` + nl) + builder.WriteString(f.outline(0)) + builder.WriteString(`` + nl) + builder.WriteString(`` + nl) + return builder.String() +} diff --git a/src/opml/opml_test.go b/src/opml/opml_test.go new file mode 100644 index 0000000..207329c --- /dev/null +++ b/src/opml/opml_test.go @@ -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 := ` + +subscriptions + + + + + + + + +` + fmt.Println(have) + if !reflect.DeepEqual(want, have) { + t.Logf("want: %s", want) + t.Logf("have: %s", have) + t.Fatal("invalid opml") + } +} diff --git a/src/opml/parser.go b/src/opml/parser.go deleted file mode 100644 index 377cf1c..0000000 --- a/src/opml/parser.go +++ /dev/null @@ -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 -} diff --git a/src/opml/read.go b/src/opml/read.go new file mode 100644 index 0000000..3e6b6c0 --- /dev/null +++ b/src/opml/read.go @@ -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 +} diff --git a/src/opml/read_test.go b/src/opml/read_test.go new file mode 100644 index 0000000..ca5dd9b --- /dev/null +++ b/src/opml/read_test.go @@ -0,0 +1,60 @@ +package opml + +import ( + "reflect" + "strings" + "testing" +) + + +func TestParse(t *testing.T) { + have, _ := Parse(strings.NewReader(` + + + Subscriptions + + + + + + + + + `)) + 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") + } +}