From 0f6f4c8c60fad0e3cf5ff154d222475e1b2ce772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Thu, 16 Dec 2021 11:42:43 -0800 Subject: [PATCH] Add tag to OPML export --- reader/opml/opml.go | 70 ++++++++++++++++++++-------------- reader/opml/parser.go | 6 +-- reader/opml/serializer.go | 65 ++++++++++++++++--------------- reader/opml/serializer_test.go | 4 +- 4 files changed, 79 insertions(+), 66 deletions(-) diff --git a/reader/opml/opml.go b/reader/opml/opml.go index 76d61939..7207d4f1 100644 --- a/reader/opml/opml.go +++ b/reader/opml/opml.go @@ -8,21 +8,49 @@ import ( "encoding/xml" ) -type opml struct { - XMLName xml.Name `xml:"opml"` - Version string `xml:"version,attr"` - Outlines []outline `xml:"body>outline"` +// Specs: http://opml.org/spec2.opml +type opmlDocument struct { + XMLName xml.Name `xml:"opml"` + Version string `xml:"version,attr"` + Header opmlHeader `xml:"head"` + Outlines []opmlOutline `xml:"body>outline"` } -type outline struct { - Title string `xml:"title,attr,omitempty"` - Text string `xml:"text,attr"` - FeedURL string `xml:"xmlUrl,attr,omitempty"` - SiteURL string `xml:"htmlUrl,attr,omitempty"` - Outlines []outline `xml:"outline,omitempty"` +func NewOPMLDocument() *opmlDocument { + return &opmlDocument{} } -func (o *outline) GetTitle() string { +func (o *opmlDocument) GetSubscriptionList() SubcriptionList { + var subscriptions SubcriptionList + for _, outline := range o.Outlines { + if len(outline.Outlines) > 0 { + for _, element := range outline.Outlines { + // outline.Text is only available in OPML v2. + subscriptions = element.Append(subscriptions, outline.Text) + } + } else { + subscriptions = outline.Append(subscriptions, "") + } + } + + return subscriptions +} + +type opmlHeader struct { + Title string `xml:"title,omitempty"` + DateCreated string `xml:"dateCreated,omitempty"` + OwnerName string `xml:"ownerName,omitempty"` +} + +type opmlOutline struct { + Title string `xml:"title,attr,omitempty"` + Text string `xml:"text,attr"` + FeedURL string `xml:"xmlUrl,attr,omitempty"` + SiteURL string `xml:"htmlUrl,attr,omitempty"` + Outlines []opmlOutline `xml:"outline,omitempty"` +} + +func (o *opmlOutline) GetTitle() string { if o.Title != "" { return o.Title } @@ -42,7 +70,7 @@ func (o *outline) GetTitle() string { return "" } -func (o *outline) GetSiteURL() string { +func (o *opmlOutline) GetSiteURL() string { if o.SiteURL != "" { return o.SiteURL } @@ -50,7 +78,7 @@ func (o *outline) GetSiteURL() string { return o.FeedURL } -func (o *outline) Append(subscriptions SubcriptionList, category string) SubcriptionList { +func (o *opmlOutline) Append(subscriptions SubcriptionList, category string) SubcriptionList { if o.FeedURL != "" { subscriptions = append(subscriptions, &Subcription{ Title: o.GetTitle(), @@ -62,19 +90,3 @@ func (o *outline) Append(subscriptions SubcriptionList, category string) Subcrip return subscriptions } - -func (o *opml) Transform() SubcriptionList { - var subscriptions SubcriptionList - for _, outline := range o.Outlines { - if len(outline.Outlines) > 0 { - for _, element := range outline.Outlines { - // outline.Text is only available in OPML v2. - subscriptions = element.Append(subscriptions, outline.Text) - } - } else { - subscriptions = outline.Append(subscriptions, "") - } - } - - return subscriptions -} diff --git a/reader/opml/parser.go b/reader/opml/parser.go index 97974bb5..a32df1de 100644 --- a/reader/opml/parser.go +++ b/reader/opml/parser.go @@ -14,16 +14,16 @@ import ( // Parse reads an OPML file and returns a SubcriptionList. func Parse(data io.Reader) (SubcriptionList, *errors.LocalizedError) { - feeds := new(opml) + opmlDocument := NewOPMLDocument() decoder := xml.NewDecoder(data) decoder.Entity = xml.HTMLEntity decoder.Strict = false decoder.CharsetReader = encoding.CharsetReader - err := decoder.Decode(feeds) + err := decoder.Decode(opmlDocument) if err != nil { return nil, errors.NewLocalizedError("Unable to parse OPML file: %q", err) } - return feeds.Transform(), nil + return opmlDocument.GetSubscriptionList(), nil } diff --git a/reader/opml/serializer.go b/reader/opml/serializer.go index 31619e14..fd3acb33 100644 --- a/reader/opml/serializer.go +++ b/reader/opml/serializer.go @@ -9,6 +9,7 @@ import ( "bytes" "encoding/xml" "sort" + "time" "miniflux.app/logger" ) @@ -19,10 +20,10 @@ func Serialize(subscriptions SubcriptionList) string { writer := bufio.NewWriter(&b) writer.WriteString(xml.Header) - feeds := normalizeFeeds(subscriptions) + opmlDocument := convertSubscriptionsToOPML(subscriptions) encoder := xml.NewEncoder(writer) - encoder.Indent(" ", " ") - if err := encoder.Encode(feeds); err != nil { + encoder.Indent("", " ") + if err := encoder.Encode(opmlDocument); err != nil { logger.Error("[OPML:Serialize] %v", err) return "" } @@ -30,6 +31,36 @@ func Serialize(subscriptions SubcriptionList) string { return b.String() } +func convertSubscriptionsToOPML(subscriptions SubcriptionList) *opmlDocument { + opmlDocument := NewOPMLDocument() + opmlDocument.Version = "2.0" + opmlDocument.Header.Title = "Miniflux" + opmlDocument.Header.DateCreated = time.Now().Format("Mon, 02 Jan 2006 15:04:05 MST") + + groupedSubs := groupSubscriptionsByFeed(subscriptions) + var categories []string + for k := range groupedSubs { + categories = append(categories, k) + } + sort.Strings(categories) + + for _, categoryName := range categories { + category := opmlOutline{Text: categoryName} + for _, subscription := range groupedSubs[categoryName] { + category.Outlines = append(category.Outlines, opmlOutline{ + Title: subscription.Title, + Text: subscription.Title, + FeedURL: subscription.FeedURL, + SiteURL: subscription.SiteURL, + }) + } + + opmlDocument.Outlines = append(opmlDocument.Outlines, category) + } + + return opmlDocument +} + func groupSubscriptionsByFeed(subscriptions SubcriptionList) map[string]SubcriptionList { groups := make(map[string]SubcriptionList) @@ -39,31 +70,3 @@ func groupSubscriptionsByFeed(subscriptions SubcriptionList) map[string]Subcript return groups } - -func normalizeFeeds(subscriptions SubcriptionList) *opml { - feeds := new(opml) - feeds.Version = "2.0" - - groupedSubs := groupSubscriptionsByFeed(subscriptions) - var categories []string - for k := range groupedSubs { - categories = append(categories, k) - } - sort.Strings(categories) - - for _, categoryName := range categories { - category := outline{Text: categoryName} - for _, subscription := range groupedSubs[categoryName] { - category.Outlines = append(category.Outlines, outline{ - Title: subscription.Title, - Text: subscription.Title, - FeedURL: subscription.FeedURL, - SiteURL: subscription.SiteURL, - }) - } - - feeds.Outlines = append(feeds.Outlines, category) - } - - return feeds -} diff --git a/reader/opml/serializer_test.go b/reader/opml/serializer_test.go index c0acd904..ff26e195 100644 --- a/reader/opml/serializer_test.go +++ b/reader/opml/serializer_test.go @@ -6,7 +6,6 @@ package opml // import "miniflux.app/reader/opml" import ( "bytes" - "fmt" "testing" ) @@ -17,7 +16,6 @@ func TestSerialize(t *testing.T) { subscriptions = append(subscriptions, &Subcription{Title: "Feed 3", FeedURL: "http://example.org/feed/3", SiteURL: "http://example.org/3", CategoryName: "Category 2"}) output := Serialize(subscriptions) - fmt.Println(output) feeds, err := Parse(bytes.NewBufferString(output)) if err != nil { t.Error(err) @@ -56,7 +54,7 @@ func TestNormalizedCategoriesOrder(t *testing.T) { subscriptions = append(subscriptions, &Subcription{Title: "Feed 2", FeedURL: "http://example.org/feed/2", SiteURL: "http://example.org/2", CategoryName: orderTests[1].naturalOrderName}) subscriptions = append(subscriptions, &Subcription{Title: "Feed 3", FeedURL: "http://example.org/feed/3", SiteURL: "http://example.org/3", CategoryName: orderTests[2].naturalOrderName}) - feeds := normalizeFeeds(subscriptions) + feeds := convertSubscriptionsToOPML(subscriptions) for i, o := range orderTests { if feeds.Outlines[i].Text != o.correctOrderName {