mirror of
				https://github.com/nkanaev/yarr.git
				synced 2025-10-29 22:29:59 +00:00 
			
		
		
		
	rewrite opml
This commit is contained in:
		| @@ -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) + `<outline type="rss" text="%s" description="%s" xmlUrl="%s" htmlUrl="%s"/>`, | ||||
| 			feed.title, feed.description, | ||||
| 			feed.feedUrl, feed.siteUrl, | ||||
| 		) | ||||
| 	} | ||||
| 	line(`<?xml version="1.0" encoding="UTF-8"?>`) | ||||
| 	line(`<opml version="1.1">`) | ||||
| 	line(`<head><title>Subscriptions</title></head>`) | ||||
| 	line(`<body>`) | ||||
| 	for _, folder := range b.folders { | ||||
| 		line(`  <outline text="%s">`, folder.title) | ||||
| 		for _, feed := range folder.feeds { | ||||
| 			feedline(feed, 4) | ||||
| 		} | ||||
| 		line(`  </outline>`) | ||||
| 	} | ||||
| 	for _, feed := range b.rootfolder.feeds { | ||||
| 		feedline(feed, 2) | ||||
| 	} | ||||
| 	line(`</body>`) | ||||
| 	line(`</opml>`) | ||||
|  | ||||
| 	return builder.String() | ||||
| } | ||||
| @@ -1,30 +0,0 @@ | ||||
| package opml | ||||
|  | ||||
| import "testing" | ||||
|  | ||||
| var sample = `<?xml version="1.0" encoding="UTF-8"?> | ||||
| <opml version="1.1"> | ||||
| <head><title>Subscriptions</title></head> | ||||
| <body> | ||||
|   <outline text="sub"> | ||||
|     <outline type="rss" text="subtitle1" description="sub1" xmlUrl="https://foo.com/feed.xml" htmlUrl="https://foo.com/"/> | ||||
|     <outline type="rss" text="&>" description="<>" xmlUrl="https://bar.com/feed.xml" htmlUrl="https://bar.com/"/> | ||||
|   </outline> | ||||
|   <outline type="rss" text="title1" description="desc1" xmlUrl="https://example.com/feed.xml" htmlUrl="https://example.com/"/> | ||||
| </body> | ||||
| </opml> | ||||
| ` | ||||
|  | ||||
| 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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										78
									
								
								src/opml/opml.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/opml/opml.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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(`<outline text="%s">` + 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 + `</outline>` + nl) | ||||
| 	} | ||||
| 	return builder.String() | ||||
| } | ||||
|  | ||||
| func (f *Feed) outline(level int) string { | ||||
| 	return strings.Repeat(indent, level) + fmt.Sprintf( | ||||
| 		`<outline type="rss" text="%s" xmlUrl="%s" htmlUrl="%s"/>` + nl, | ||||
| 		e(f.Title), e(f.FeedUrl), e(f.SiteUrl), | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func (f *Folder) OPML() string { | ||||
| 	builder := strings.Builder{} | ||||
| 	builder.WriteString(`<?xml version="1.0" encoding="UTF-8"?>` + nl) | ||||
| 	builder.WriteString(`<opml version="1.1">` + nl) | ||||
| 	builder.WriteString(`<head><title>subscriptions</title></head>` + nl) | ||||
| 	builder.WriteString(`<body>` + nl) | ||||
| 	builder.WriteString(f.outline(0)) | ||||
| 	builder.WriteString(`</body>` + nl) | ||||
| 	builder.WriteString(`</opml>` + nl) | ||||
| 	return builder.String() | ||||
| } | ||||
							
								
								
									
										57
									
								
								src/opml/opml_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/opml/opml_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 := `<?xml version="1.0" encoding="UTF-8"?> | ||||
| <opml version="1.1"> | ||||
| <head><title>subscriptions</title></head> | ||||
| <body> | ||||
|   <outline text="sub"> | ||||
|     <outline type="rss" text="subtitle1" xmlUrl="https://foo.com/feed.xml" htmlUrl="https://foo.com/"/> | ||||
|     <outline type="rss" text="&>" xmlUrl="https://bar.com/feed.xml" htmlUrl="https://bar.com/"/> | ||||
|   </outline> | ||||
|   <outline type="rss" text="title1" xmlUrl="https://baz.com/feed.xml" htmlUrl="https://baz.com/"/> | ||||
| </body> | ||||
| </opml> | ||||
| ` | ||||
| 	fmt.Println(have) | ||||
| 	if !reflect.DeepEqual(want, have) { | ||||
| 		t.Logf("want: %s", want) | ||||
| 		t.Logf("have: %s", have) | ||||
| 		t.Fatal("invalid opml") | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
							
								
								
									
										49
									
								
								src/opml/read.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/opml/read.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										60
									
								
								src/opml/read_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/opml/read_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| package opml | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
|  | ||||
| func TestParse(t *testing.T) { | ||||
| 	have, _ := Parse(strings.NewReader(` | ||||
| 		<?xml version="1.0" encoding="UTF-8"?> | ||||
| 		<opml version="1.1"> | ||||
| 		<head><title>Subscriptions</title></head> | ||||
| 		<body> | ||||
| 			<outline text="sub"> | ||||
| 				<outline type="rss" text="subtitle1" description="sub1" | ||||
| 						 xmlUrl="https://foo.com/feed.xml" htmlUrl="https://foo.com/"/> | ||||
| 				<outline type="rss" text="&>" description="<>" | ||||
| 						 xmlUrl="https://bar.com/feed.xml" htmlUrl="https://bar.com/"/> | ||||
| 			</outline> | ||||
| 			<outline type="rss" text="title1" description="desc1" | ||||
| 					 xmlUrl="https://baz.com/feed.xml" htmlUrl="https://baz.com/"/> | ||||
| 		</body> | ||||
| 		</opml> | ||||
| 	`)) | ||||
| 	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") | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user