1
0
Fork 0

Add <head> tag to OPML export

This commit is contained in:
Frédéric Guillot 2021-12-16 11:42:43 -08:00
parent 47b47cc32c
commit 0f6f4c8c60
4 changed files with 79 additions and 66 deletions

View file

@ -8,21 +8,49 @@ import (
"encoding/xml" "encoding/xml"
) )
type opml struct { // Specs: http://opml.org/spec2.opml
type opmlDocument 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"` Header opmlHeader `xml:"head"`
Outlines []opmlOutline `xml:"body>outline"`
} }
type outline struct { func NewOPMLDocument() *opmlDocument {
return &opmlDocument{}
}
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"` Title string `xml:"title,attr,omitempty"`
Text string `xml:"text,attr"` Text 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"`
Outlines []outline `xml:"outline,omitempty"` Outlines []opmlOutline `xml:"outline,omitempty"`
} }
func (o *outline) GetTitle() string { func (o *opmlOutline) GetTitle() string {
if o.Title != "" { if o.Title != "" {
return o.Title return o.Title
} }
@ -42,7 +70,7 @@ func (o *outline) GetTitle() string {
return "" return ""
} }
func (o *outline) GetSiteURL() string { func (o *opmlOutline) GetSiteURL() string {
if o.SiteURL != "" { if o.SiteURL != "" {
return o.SiteURL return o.SiteURL
} }
@ -50,7 +78,7 @@ func (o *outline) GetSiteURL() string {
return o.FeedURL return o.FeedURL
} }
func (o *outline) Append(subscriptions SubcriptionList, category string) SubcriptionList { func (o *opmlOutline) Append(subscriptions SubcriptionList, category string) SubcriptionList {
if o.FeedURL != "" { if o.FeedURL != "" {
subscriptions = append(subscriptions, &Subcription{ subscriptions = append(subscriptions, &Subcription{
Title: o.GetTitle(), Title: o.GetTitle(),
@ -62,19 +90,3 @@ func (o *outline) Append(subscriptions SubcriptionList, category string) Subcrip
return subscriptions 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
}

View file

@ -14,16 +14,16 @@ import (
// Parse reads an OPML file and returns a SubcriptionList. // Parse reads an OPML file and returns a SubcriptionList.
func Parse(data io.Reader) (SubcriptionList, *errors.LocalizedError) { func Parse(data io.Reader) (SubcriptionList, *errors.LocalizedError) {
feeds := new(opml) opmlDocument := NewOPMLDocument()
decoder := xml.NewDecoder(data) decoder := xml.NewDecoder(data)
decoder.Entity = xml.HTMLEntity decoder.Entity = xml.HTMLEntity
decoder.Strict = false decoder.Strict = false
decoder.CharsetReader = encoding.CharsetReader decoder.CharsetReader = encoding.CharsetReader
err := decoder.Decode(feeds) err := decoder.Decode(opmlDocument)
if err != nil { if err != nil {
return nil, errors.NewLocalizedError("Unable to parse OPML file: %q", err) return nil, errors.NewLocalizedError("Unable to parse OPML file: %q", err)
} }
return feeds.Transform(), nil return opmlDocument.GetSubscriptionList(), nil
} }

View file

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"sort" "sort"
"time"
"miniflux.app/logger" "miniflux.app/logger"
) )
@ -19,10 +20,10 @@ func Serialize(subscriptions SubcriptionList) string {
writer := bufio.NewWriter(&b) writer := bufio.NewWriter(&b)
writer.WriteString(xml.Header) writer.WriteString(xml.Header)
feeds := normalizeFeeds(subscriptions) opmlDocument := convertSubscriptionsToOPML(subscriptions)
encoder := xml.NewEncoder(writer) encoder := xml.NewEncoder(writer)
encoder.Indent(" ", " ") encoder.Indent("", " ")
if err := encoder.Encode(feeds); err != nil { if err := encoder.Encode(opmlDocument); err != nil {
logger.Error("[OPML:Serialize] %v", err) logger.Error("[OPML:Serialize] %v", err)
return "" return ""
} }
@ -30,6 +31,36 @@ func Serialize(subscriptions SubcriptionList) string {
return b.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 { func groupSubscriptionsByFeed(subscriptions SubcriptionList) map[string]SubcriptionList {
groups := make(map[string]SubcriptionList) groups := make(map[string]SubcriptionList)
@ -39,31 +70,3 @@ func groupSubscriptionsByFeed(subscriptions SubcriptionList) map[string]Subcript
return groups 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
}

View file

@ -6,7 +6,6 @@ package opml // import "miniflux.app/reader/opml"
import ( import (
"bytes" "bytes"
"fmt"
"testing" "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"}) subscriptions = append(subscriptions, &Subcription{Title: "Feed 3", FeedURL: "http://example.org/feed/3", SiteURL: "http://example.org/3", CategoryName: "Category 2"})
output := Serialize(subscriptions) output := Serialize(subscriptions)
fmt.Println(output)
feeds, err := Parse(bytes.NewBufferString(output)) feeds, err := Parse(bytes.NewBufferString(output))
if err != nil { if err != nil {
t.Error(err) 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 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}) 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 { for i, o := range orderTests {
if feeds.Outlines[i].Text != o.correctOrderName { if feeds.Outlines[i].Text != o.correctOrderName {