commit 8dfb72dda0321813659e4a002f69ac988a0143fd from: Oliver Lowe date: Sun Apr 13 08:47:08 2025 UTC add opml package Feed readers use OPML to exchange lists of feeds, which is what any applications developed here may do at some point, too. This package only does decoding for now; I dumped my own feeds out of my own feed reader app as a quick test to see how tricky this would be! commit - 848b29c4c69a1837ec426676170747fd0d830885 commit + 8dfb72dda0321813659e4a002f69ac988a0143fd blob - /dev/null blob + 42ceb2ef3c9ea7e29c9faebd1d1949424320ed04 (mode 644) --- /dev/null +++ opml/opml.go @@ -0,0 +1,43 @@ +// Package opml implements decoding of OPML documents described at [opml.org]. +// Only a subset of the format is implemented; +// enough to support the use of OPML as an interchange format by +// feed reader applications for web feed collections. +// +// [opml.org]: https://opml.org +package opml + +import ( + "encoding/xml" + "io" +) + +type Document struct { + XMLName struct{} `xml:"opml"` + Version string `xml:"version,attr"` + Body []Outline `xml:"body>outline"` + + // head holds the document's metadata. + // TODO(otl): should we even support this? + // NetNewWire generates an empty tree. + head struct{} +} + +type Outline struct { + XMLName struct{} `xml:"outline"` + Text string `xml:"text,attr"` + Title string `xml:"title,attr"` + Description string `xml:"description,attr,omitempty"` + Type string `xml:"type,attr"` + Version string `xml:"version,attr,omitempty"` + HTML string `xml:"htmlUrl,attr"` + XML string `xml:"xmlUrl,attr"` + Children []Outline `xml:"outline,omitempty"` +} + +func Decode(r io.Reader) (*Document, error) { + var doc Document + if err := xml.NewDecoder(r).Decode(&doc); err != nil { + return nil, err + } + return &doc, nil +} blob - /dev/null blob + 09d1e40cc29f67221327a8158acec8ccbfaa1379 (mode 644) --- /dev/null +++ opml/opml_test.go @@ -0,0 +1,42 @@ +package opml + +import ( + "os" + "testing" +) + +func TestDecode(t *testing.T) { + f, err := os.Open("otl.opml") + if err != nil { + t.Fatal(err) + } + doc, err := Decode(f) + if err != nil { + t.Fatal(err) + } + + // The tricky thing is decoding child elements recursively + // just using struct tags. So checking the number of elements + // is expected gives us some confidence we're reading + // the document correctly. + var counts = map[string]int{ + "aggregators": 6, + "apple": 6, + "corp": 4, + "email": 4, + "Friends": 5, + "lang": 4, + "misc": 92, + "repos": 6, + } + for _, node := range doc.Body { + want, ok := counts[node.Title] + if !ok { + t.Errorf("unknown node title %s", node.Title) + continue + } + if len(node.Children) != want { + t.Errorf("%d child elements in %s, want %d", len(node.Children), node.Title, want) + } + } +} blob - /dev/null blob + 1cc9855d4414da78d4b81e667f79ddc9db717ea6 (mode 644) --- /dev/null +++ opml/otl.opml @@ -0,0 +1,152 @@ + + + + + Subscriptions-iCloud.opml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file