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")
+ }
+}