~chrisppy/go-opml

5b3787d55248362281f89c0833b9865c6b8d73fc — Chris Palmer 5 months ago
Initial Commit
9 files changed, 704 insertions(+), 0 deletions(-)

A .build.yml
A .gitignore
A README.md
A go.mod
A go.sum
A opml.go
A opml_test.go
A test-data/states.opml
A test-data/subscriptionList.opml
A  => .build.yml +35 -0
@@ 1,35 @@
image: alpine/edge
packages:
- go
sources:
- https://git.sr.ht/~chrisppy/go-opml
tasks:
- format: |
    cd go-opml
    go fmt -x ./...
    go vet ./...
- lint: |
    cd go-opml
    go get -u golang.org/x/lint/golint
    go get -u honnef.co/go/tools/cmd/staticcheck
    go get -u gitlab.com/opennota/check/cmd/aligncheck
    go get -u gitlab.com/opennota/check/cmd/structcheck
    go get -u gitlab.com/opennota/check/cmd/varcheck
    go get -u github.com/kisielk/errcheck
    golint -set_exit_status ./...
    staticcheck ./...
    aligncheck ./...
    structcheck ./...
    varcheck ./...
    errcheck ./...
- security: |
    go get -u github.com/securego/gosec/cmd/gosec
    cd go-opml
    gosec -exclude=G107,G204 ./...
- dependencies: |
    go get -u github.com/psampaz/go-mod-outdated
    cd go-opml
    go list -u -m -json all | go-mod-outdated -direct -ci
- test: |
    cd go-opml
    go test ./...

A  => .gitignore +3 -0
@@ 1,3 @@
*.test
*.prof
*.log

A  => README.md +3 -0
@@ 1,3 @@
# go-opml [![GoDoc](https://godoc.org/git.sr.ht/~chrisppy/go-opml?status.svg)](https://godoc.org/git.sr.ht/~chrisppy/go-opml) [![builds.sr.ht status](https://builds.sr.ht/~chrisppy/go-opml.svg)](https://builds.sr.ht/~chrisppy/go-opml?)

An implementation of the [OPML](http://dev.opml.org/) message format for Go.

A  => go.mod +8 -0
@@ 1,8 @@
module git.sr.ht/~chrisppy/go-opml

go 1.15

require (
	github.com/stretchr/testify v1.6.1
	golang.org/x/net v0.0.0-20201224014010-6772e930b67b
)

A  => go.sum +17 -0
@@ 1,17 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

A  => opml.go +126 -0
@@ 1,126 @@
// This file is part of beagles.
//
// Copyright © 2020 Chris Palmer <chris@red-oxide.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package opml

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"path/filepath"
	"strings"

	"golang.org/x/net/html/charset"
)

// OPML elements
type OPML struct {
	XMLName xml.Name `xml:"opml"`
	Version string   `xml:"version,attr"`
	Head    Head     `xml:"head"`
	Body    Body     `xml:"body"`
}

// Head Elements
type Head struct {
	XMLName         xml.Name `xml:"head"`
	Title           string   `xml:"title,omitempty"`
	DateCreated     string   `xml:"dateCreated,omitempty"`
	DateModified    string   `xml:"dateModified,omitempty"`
	OwnerName       string   `xml:"ownerName,omitempty"`
	OwnerEmail      string   `xml:"ownerEmail,omitempty"`
	OwnerID         string   `xml:"ownerId,omitempty"`
	Docs            string   `xml:"docs,omitempty"`
	ExpansionState  string   `xml:"expansionState,omitempty"`
	VertScrollState int      `xml:"vertScrollState,omitempty"`
	WindowTop       int      `xml:"windowTop,omitempty"`
	WindowLeft      int      `xml:"windowLeft,omitempty"`
	WindowBottom    int      `xml:"windowBottom,omitempty"`
	WindowRight     int      `xml:"windowRight,omitempty"`
}

// Body Elements
type Body struct {
	XMLName  xml.Name   `xml:"body"`
	Outlines []*Outline `xml:"outline"`
}

// Outline elements
type Outline struct {
	XMLName     xml.Name   `xml:"outline"`
	Text        string     `xml:"text,attr"`
	Description string     `xml:"description,attr,omitempty"`
	HTMLURL     string     `xml:"htmlUrl,attr,omitempty"`
	Language    string     `xml:"language,attr,omitempty"`
	Title       string     `xml:"title,attr,omitempty"`
	Type        string     `xml:"type,attr,omitempty"`
	Version     string     `xml:"version,attr,omitempty"`
	XMLURL      string     `xml:"xmlUrl,attr,omitempty"`
	Created     string     `xml:"created,attr,omitempty"`
	Outlines    []*Outline `xml:"outline"`
}

// FromFile will read the xml file
func FromFile(path string) (*OPML, error) {
	b, err := ioutil.ReadFile(filepath.Clean(path))
	if err != nil {
		return nil, err
	}
	r := bytes.NewReader(b)
	return Read(r)
}

// Read the xml from a reader
func Read(r io.Reader) (*OPML, error) {
	var o OPML
	decoder := xml.NewDecoder(r)
	decoder.CharsetReader = charset.NewReaderLabel
	if err := decoder.Decode(&o); err != nil {
		return nil, err
	}

	return &o, nil
}

// XML will convert the structure to XML
func (o *OPML) XML() ([]byte, error) {
	b := &bytes.Buffer{}
	enc := xml.NewEncoder(b)
	enc.Indent("", "\t")
	if err := enc.Encode(o); err != nil {
		return nil, err
	}

	s := fmt.Sprintf(`<?xml version="1.0" encoding="ISO-8859-1"?>
%s
`, b.String())

	s = strings.ReplaceAll(s, "></outline>", "/>")
	return []byte(s), nil
}

// Write the structure to a file as XML
func (o *OPML) Write(path string) error {
	b, err := o.XML()
	if err != nil {
		return err
	}

	return ioutil.WriteFile(filepath.Clean(path), b, 0600)
}

A  => opml_test.go +391 -0
@@ 1,391 @@
package opml

import (
	"bytes"
	"io/ioutil"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPathDNE(t *testing.T) {
	data, err := FromFile("invalidPath.opml")
	assert.Error(t, err)
	assert.Nil(t, data)
}

func TestErrorData(t *testing.T) {
	data, err := Read(bytes.NewReader([]byte("Invalid Data")))
	assert.Error(t, err)
	assert.Nil(t, data)
}

func TestReadSubscriptionList(t *testing.T) {
	data, err := FromFile("test-data/subscriptionList.opml")
	assert.NoError(t, err)
	assert.NotNil(t, data)
	assert.Equal(t, "2.0", data.Version)

	// Test Head
	assert.Equal(t, "mySubscriptions.opml", data.Head.Title)
	assert.Equal(t, "Sat, 18 Jun 2005 12:11:52 GMT", data.Head.DateCreated)
	assert.Equal(t, "Tue, 02 Aug 2005 21:42:48 GMT", data.Head.DateModified)
	assert.Equal(t, "Dave Winer", data.Head.OwnerName)
	assert.Equal(t, "dave@scripting.com", data.Head.OwnerEmail)
	assert.Equal(t, "", data.Head.OwnerID)
	assert.Equal(t, "", data.Head.Docs)
	assert.Equal(t, "", data.Head.ExpansionState)
	assert.Equal(t, 1, data.Head.VertScrollState)
	assert.Equal(t, 61, data.Head.WindowTop)
	assert.Equal(t, 304, data.Head.WindowLeft)
	assert.Equal(t, 562, data.Head.WindowBottom)
	assert.Equal(t, 842, data.Head.WindowRight)

	// Test Body
	assert.Equal(t, 13, len(data.Body.Outlines))
	for _, o := range data.Body.Outlines {
		assert.Equal(t, "", o.Created)
		assert.NotEqual(t, "", o.Text)
		assert.NotEqual(t, "", o.Version)
		assert.NotEqual(t, "", o.Type)
		assert.NotNil(t, o.Language)
		assert.NotNil(t, o.Title)
		assert.NotNil(t, o.Description)
		assert.NotNil(t, o.HTMLURL)
		assert.NotNil(t, o.XMLURL)
	}
}

func TestReadStates(t *testing.T) {
	data, err := FromFile("test-data/states.opml")
	assert.NoError(t, err)
	assert.NotNil(t, data)
	assert.Equal(t, "2.0", data.Version)

	// Test Head
	assert.Equal(t, "states.opml", data.Head.Title)
	assert.Equal(t, "Tue, 15 Mar 2005 16:35:45 GMT", data.Head.DateCreated)
	assert.Equal(t, "Thu, 14 Jul 2005 23:41:05 GMT", data.Head.DateModified)
	assert.Equal(t, "Dave Winer", data.Head.OwnerName)
	assert.Equal(t, "dave@scripting.com", data.Head.OwnerEmail)
	assert.Equal(t, "", data.Head.OwnerID)
	assert.Equal(t, "", data.Head.Docs)
	assert.Equal(t, "1, 6, 13, 16, 18, 20", data.Head.ExpansionState)
	assert.Equal(t, 1, data.Head.VertScrollState)
	assert.Equal(t, 106, data.Head.WindowTop)
	assert.Equal(t, 106, data.Head.WindowLeft)
	assert.Equal(t, 558, data.Head.WindowBottom)
	assert.Equal(t, 479, data.Head.WindowRight)

	// Test Body
	assert.Equal(t, 1, len(data.Body.Outlines))
	p := data.Body.Outlines[0]
	assert.Equal(t, "United States", p.Text)
	assert.Equal(t, "", p.Version)
	assert.Equal(t, "", p.Type)
	assert.Equal(t, "", p.Created)
	assert.Equal(t, "", p.Language)
	assert.Equal(t, "", p.Title)
	assert.Equal(t, "", p.Description)
	assert.Equal(t, "", p.HTMLURL)
	assert.Equal(t, "", p.XMLURL)
	assert.Equal(t, 8, len(p.Outlines))

	for i, o := range p.Outlines {
		assert.Equal(t, "", o.Version)
		assert.Equal(t, "", o.Type)
		assert.Equal(t, "", o.Created)
		assert.Equal(t, "", o.Language)
		assert.Equal(t, "", o.Title)
		assert.Equal(t, "", o.Description)
		assert.Equal(t, "", o.HTMLURL)
		assert.Equal(t, "", o.XMLURL)
		switch i {
		case 0:
			assert.Equal(t, "Far West", o.Text)
			assert.Equal(t, 6, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				switch j {
				case 0:
					assert.Equal(t, "Alaska", n.Text)
					assert.Equal(t, 0, len(n.Outlines))
				case 1:
					assert.Equal(t, "California", n.Text)
					assert.Equal(t, 0, len(n.Outlines))
				case 2:
					assert.Equal(t, "Hawaii", n.Text)
					assert.Equal(t, 0, len(n.Outlines))
				case 3:
					assert.Equal(t, "Nevada", n.Text)
					assert.Equal(t, 4, len(n.Outlines))
					for k, m := range n.Outlines {
						assert.NotEqual(t, "", m.Created)
						assert.Equal(t, "", m.Version)
						assert.Equal(t, "", m.Type)
						assert.Equal(t, "", m.Language)
						assert.Equal(t, "", m.Title)
						assert.Equal(t, "", m.Description)
						assert.Equal(t, "", m.HTMLURL)
						assert.Equal(t, "", m.XMLURL)
						switch k {
						case 0:
							assert.Equal(t, "Reno", m.Text)
						case 1:
							assert.Equal(t, "Las Vegas", m.Text)
						case 2:
							assert.Equal(t, "Ely", m.Text)
						case 3:
							assert.Equal(t, "Gerlach", m.Text)
						}
					}
				case 4:
					assert.Equal(t, "Oregon", n.Text)
					assert.Equal(t, 0, len(n.Outlines))
				case 5:
					assert.Equal(t, "Washington", n.Text)
					assert.Equal(t, 0, len(n.Outlines))
				}
			}
		case 1:
			assert.Equal(t, "Great Plains", o.Text)
			assert.Equal(t, 5, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				assert.Equal(t, 0, len(n.Outlines))
				switch j {
				case 0:
					assert.Equal(t, "Kansas", n.Text)
				case 1:
					assert.Equal(t, "Nebraska", n.Text)
				case 2:
					assert.Equal(t, "North Dakota", n.Text)
				case 3:
					assert.Equal(t, "Oklahoma", n.Text)
				case 4:
					assert.Equal(t, "South Dakota", n.Text)
				}
			}
		case 2:
			assert.Equal(t, "Mid-Atlantic", o.Text)
			assert.Equal(t, 5, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				assert.Equal(t, 0, len(n.Outlines))
				switch j {
				case 0:
					assert.Equal(t, "Delaware", n.Text)
				case 1:
					assert.Equal(t, "Maryland", n.Text)
				case 2:
					assert.Equal(t, "New Jersey", n.Text)
				case 3:
					assert.Equal(t, "New York", n.Text)
				case 4:
					assert.Equal(t, "Pennsylvania", n.Text)
				}
			}
		case 3:
			assert.Equal(t, "Midwest", o.Text)
			assert.Equal(t, 10, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				assert.Equal(t, 0, len(n.Outlines))
				switch j {
				case 0:
					assert.Equal(t, "Illinois", n.Text)
				case 1:
					assert.Equal(t, "Indiana", n.Text)
				case 2:
					assert.Equal(t, "Iowa", n.Text)
				case 3:
					assert.Equal(t, "Kentucky", n.Text)
				case 4:
					assert.Equal(t, "Michigan", n.Text)
				case 5:
					assert.Equal(t, "Minnesota", n.Text)
				case 6:
					assert.Equal(t, "Missouri", n.Text)
				case 7:
					assert.Equal(t, "Ohio", n.Text)
				case 8:
					assert.Equal(t, "West Virginia", n.Text)
				case 9:
					assert.Equal(t, "Wisconsin", n.Text)
				}
			}
		case 4:
			assert.Equal(t, "Mountains", o.Text)
			assert.Equal(t, 5, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				assert.Equal(t, 0, len(n.Outlines))
				switch j {
				case 0:
					assert.Equal(t, "Colorado", n.Text)
				case 1:
					assert.Equal(t, "Idaho", n.Text)
				case 2:
					assert.Equal(t, "Montana", n.Text)
				case 3:
					assert.Equal(t, "Utah", n.Text)
				case 4:
					assert.Equal(t, "Wyoming", n.Text)
				}
			}
		case 5:
			assert.Equal(t, "New England", o.Text)
			assert.Equal(t, 6, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				assert.Equal(t, 0, len(n.Outlines))
				switch j {
				case 0:
					assert.Equal(t, "Connecticut", n.Text)
				case 1:
					assert.Equal(t, "Maine", n.Text)
				case 2:
					assert.Equal(t, "Massachusetts", n.Text)
				case 3:
					assert.Equal(t, "New Hampshire", n.Text)
				case 4:
					assert.Equal(t, "Rhode Island", n.Text)
				case 5:
					assert.Equal(t, "Vermont", n.Text)
				}
			}
		case 6:
			assert.Equal(t, "South", o.Text)
			assert.Equal(t, 10, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				assert.Equal(t, 0, len(n.Outlines))
				switch j {
				case 0:
					assert.Equal(t, "Alabama", n.Text)
				case 1:
					assert.Equal(t, "Arkansas", n.Text)
				case 2:
					assert.Equal(t, "Florida", n.Text)
				case 3:
					assert.Equal(t, "Georgia", n.Text)
				case 4:
					assert.Equal(t, "Louisiana", n.Text)
				case 5:
					assert.Equal(t, "Mississippi", n.Text)
				case 6:
					assert.Equal(t, "North Carolina", n.Text)
				case 7:
					assert.Equal(t, "South Carolina", n.Text)
				case 8:
					assert.Equal(t, "Tennessee", n.Text)
				case 9:
					assert.Equal(t, "Virginia", n.Text)
				}
			}
		case 7:
			assert.Equal(t, "Southwest", o.Text)
			assert.Equal(t, 3, len(o.Outlines))
			for j, n := range o.Outlines {
				assert.Equal(t, "", n.Version)
				assert.Equal(t, "", n.Type)
				assert.Equal(t, "", n.Created)
				assert.Equal(t, "", n.Language)
				assert.Equal(t, "", n.Title)
				assert.Equal(t, "", n.Description)
				assert.Equal(t, "", n.HTMLURL)
				assert.Equal(t, "", n.XMLURL)
				assert.Equal(t, 0, len(n.Outlines))
				switch j {
				case 0:
					assert.Equal(t, "Arizona", n.Text)
				case 1:
					assert.Equal(t, "New Mexico", n.Text)
				case 2:
					assert.Equal(t, "Texas", n.Text)
				}
			}
		}
	}
}

func TestWriteSubscriptionList(t *testing.T) {
	b, err := ioutil.ReadFile("test-data/subscriptionList.opml")
	assert.NoError(t, err)
	assert.NotNil(t, b)

	data, err := Read(bytes.NewReader(b))
	assert.NoError(t, err)
	assert.NotNil(t, data)

	out, err := data.XML()
	assert.NoError(t, err)
	assert.NotNil(t, out)

	assert.Equal(t, string(b), string(out))
}

func TestWriteState(t *testing.T) {
	b, err := ioutil.ReadFile("test-data/states.opml")
	assert.NoError(t, err)
	assert.NotNil(t, b)

	data, err := Read(bytes.NewReader(b))
	assert.NoError(t, err)
	assert.NotNil(t, data)

	out, err := data.XML()
	assert.NoError(t, err)
	assert.NotNil(t, out)

	assert.Equal(t, string(b), string(out))
}

A  => test-data/states.opml +91 -0
@@ 1,91 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<opml version="2.0">
	<head>
		<title>states.opml</title>
		<dateCreated>Tue, 15 Mar 2005 16:35:45 GMT</dateCreated>
		<dateModified>Thu, 14 Jul 2005 23:41:05 GMT</dateModified>
		<ownerName>Dave Winer</ownerName>
		<ownerEmail>dave@scripting.com</ownerEmail>
		<expansionState>1, 6, 13, 16, 18, 20</expansionState>
		<vertScrollState>1</vertScrollState>
		<windowTop>106</windowTop>
		<windowLeft>106</windowLeft>
		<windowBottom>558</windowBottom>
		<windowRight>479</windowRight>
	</head>
	<body>
		<outline text="United States">
			<outline text="Far West">
				<outline text="Alaska"/>
				<outline text="California"/>
				<outline text="Hawaii"/>
				<outline text="Nevada">
					<outline text="Reno" created="Tue, 12 Jul 2005 23:56:35 GMT"/>
					<outline text="Las Vegas" created="Tue, 12 Jul 2005 23:56:37 GMT"/>
					<outline text="Ely" created="Tue, 12 Jul 2005 23:56:39 GMT"/>
					<outline text="Gerlach" created="Tue, 12 Jul 2005 23:56:47 GMT"/>
				</outline>
				<outline text="Oregon"/>
				<outline text="Washington"/>
			</outline>
			<outline text="Great Plains">
				<outline text="Kansas"/>
				<outline text="Nebraska"/>
				<outline text="North Dakota"/>
				<outline text="Oklahoma"/>
				<outline text="South Dakota"/>
			</outline>
			<outline text="Mid-Atlantic">
				<outline text="Delaware"/>
				<outline text="Maryland"/>
				<outline text="New Jersey"/>
				<outline text="New York"/>
				<outline text="Pennsylvania"/>
			</outline>
			<outline text="Midwest">
				<outline text="Illinois"/>
				<outline text="Indiana"/>
				<outline text="Iowa"/>
				<outline text="Kentucky"/>
				<outline text="Michigan"/>
				<outline text="Minnesota"/>
				<outline text="Missouri"/>
				<outline text="Ohio"/>
				<outline text="West Virginia"/>
				<outline text="Wisconsin"/>
			</outline>
			<outline text="Mountains">
				<outline text="Colorado"/>
				<outline text="Idaho"/>
				<outline text="Montana"/>
				<outline text="Utah"/>
				<outline text="Wyoming"/>
			</outline>
			<outline text="New England">
				<outline text="Connecticut"/>
				<outline text="Maine"/>
				<outline text="Massachusetts"/>
				<outline text="New Hampshire"/>
				<outline text="Rhode Island"/>
				<outline text="Vermont"/>
			</outline>
			<outline text="South">
				<outline text="Alabama"/>
				<outline text="Arkansas"/>
				<outline text="Florida"/>
				<outline text="Georgia"/>
				<outline text="Louisiana"/>
				<outline text="Mississippi"/>
				<outline text="North Carolina"/>
				<outline text="South Carolina"/>
				<outline text="Tennessee"/>
				<outline text="Virginia"/>
			</outline>
			<outline text="Southwest">
				<outline text="Arizona"/>
				<outline text="New Mexico"/>
				<outline text="Texas"/>
			</outline>
		</outline>
	</body>
</opml>

A  => test-data/subscriptionList.opml +30 -0
@@ 1,30 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<opml version="2.0">
	<head>
		<title>mySubscriptions.opml</title>
		<dateCreated>Sat, 18 Jun 2005 12:11:52 GMT</dateCreated>
		<dateModified>Tue, 02 Aug 2005 21:42:48 GMT</dateModified>
		<ownerName>Dave Winer</ownerName>
		<ownerEmail>dave@scripting.com</ownerEmail>
		<vertScrollState>1</vertScrollState>
		<windowTop>61</windowTop>
		<windowLeft>304</windowLeft>
		<windowBottom>562</windowBottom>
		<windowRight>842</windowRight>
	</head>
	<body>
		<outline text="CNET News.com" description="Tech news and business reports by CNET News.com. Focused on information technology, core topics include computers, hardware, software, networking, and Internet media." htmlUrl="http://news.com.com/" language="unknown" title="CNET News.com" type="rss" version="RSS2" xmlUrl="http://news.com.com/2547-1_3-0-5.xml"/>
		<outline text="washingtonpost.com - Politics" description="Politics" htmlUrl="http://www.washingtonpost.com/wp-dyn/politics?nav=rss_politics" language="unknown" title="washingtonpost.com - Politics" type="rss" version="RSS2" xmlUrl="http://www.washingtonpost.com/wp-srv/politics/rssheadlines.xml"/>
		<outline text="Scobleizer: Microsoft Geek Blogger" description="Robert Scoble&#39;s look at geek and Microsoft life." htmlUrl="http://radio.weblogs.com/0001011/" language="unknown" title="Scobleizer: Microsoft Geek Blogger" type="rss" version="RSS2" xmlUrl="http://radio.weblogs.com/0001011/rss.xml"/>
		<outline text="Yahoo! News: Technology" description="Technology" htmlUrl="http://news.yahoo.com/news?tmpl=index&amp;cid=738" language="unknown" title="Yahoo! News: Technology" type="rss" version="RSS2" xmlUrl="http://rss.news.yahoo.com/rss/tech"/>
		<outline text="Workbench" description="Programming and publishing news and comment" htmlUrl="http://www.cadenhead.org/workbench/" language="unknown" title="Workbench" type="rss" version="RSS2" xmlUrl="http://www.cadenhead.org/workbench/rss.xml"/>
		<outline text="Christian Science Monitor | Top Stories" description="Read the front page stories of csmonitor.com." htmlUrl="http://csmonitor.com" language="unknown" title="Christian Science Monitor | Top Stories" type="rss" version="RSS" xmlUrl="http://www.csmonitor.com/rss/top.rss"/>
		<outline text="Dictionary.com Word of the Day" description="A new word is presented every day with its definition and example sentences from actual published works." htmlUrl="http://dictionary.reference.com/wordoftheday/" language="unknown" title="Dictionary.com Word of the Day" type="rss" version="RSS" xmlUrl="http://www.dictionary.com/wordoftheday/wotd.rss"/>
		<outline text="The Motley Fool" description="To Educate, Amuse, and Enrich" htmlUrl="http://www.fool.com" language="unknown" title="The Motley Fool" type="rss" version="RSS" xmlUrl="http://www.fool.com/xml/foolnews_rss091.xml"/>
		<outline text="InfoWorld: Top News" description="The latest on Top News from InfoWorld" htmlUrl="http://www.infoworld.com/news/index.html" language="unknown" title="InfoWorld: Top News" type="rss" version="RSS2" xmlUrl="http://www.infoworld.com/rss/news.xml"/>
		<outline text="NYT &gt; Business" description="Find breaking news &amp; business news on Wall Street, media &amp; advertising, international business, banking, interest rates, the stock market, currencies &amp; funds." htmlUrl="http://www.nytimes.com/pages/business/index.html?partner=rssnyt" language="unknown" title="NYT &gt; Business" type="rss" version="RSS2" xmlUrl="http://www.nytimes.com/services/xml/rss/nyt/Business.xml"/>
		<outline text="NYT &gt; Technology" htmlUrl="http://www.nytimes.com/pages/technology/index.html?partner=rssnyt" language="unknown" title="NYT &gt; Technology" type="rss" version="RSS2" xmlUrl="http://www.nytimes.com/services/xml/rss/nyt/Technology.xml"/>
		<outline text="Scripting News" description="It&#39;s even worse than it appears." htmlUrl="http://www.scripting.com/" language="unknown" title="Scripting News" type="rss" version="RSS2" xmlUrl="http://www.scripting.com/rss.xml"/>
		<outline text="Wired News" description="Technology, and the way we do business, is changing the world we know. Wired News is a technology - and business-oriented news service feeding an intelligent, discerning audience. What role does technology play in the day-to-day living of your life? Wired News tells you. How has evolving technology changed the face of the international business world? Wired News puts you in the picture." htmlUrl="http://www.wired.com/" language="unknown" title="Wired News" type="rss" version="RSS" xmlUrl="http://www.wired.com/news_drop/netcenter/netcenter.rdf"/>
	</body>
</opml>