Add <head> tag to OPML export
This commit is contained in:
parent
47b47cc32c
commit
0f6f4c8c60
4 changed files with 79 additions and 66 deletions
|
@ -8,21 +8,49 @@ import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type opml struct {
|
// Specs: http://opml.org/spec2.opml
|
||||||
XMLName xml.Name `xml:"opml"`
|
type opmlDocument struct {
|
||||||
Version string `xml:"version,attr"`
|
XMLName xml.Name `xml:"opml"`
|
||||||
Outlines []outline `xml:"body>outline"`
|
Version string `xml:"version,attr"`
|
||||||
|
Header opmlHeader `xml:"head"`
|
||||||
|
Outlines []opmlOutline `xml:"body>outline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type outline struct {
|
func NewOPMLDocument() *opmlDocument {
|
||||||
Title string `xml:"title,attr,omitempty"`
|
return &opmlDocument{}
|
||||||
Text string `xml:"text,attr"`
|
|
||||||
FeedURL string `xml:"xmlUrl,attr,omitempty"`
|
|
||||||
SiteURL string `xml:"htmlUrl,attr,omitempty"`
|
|
||||||
Outlines []outline `xml:"outline,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 != "" {
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue