771 lines
16 KiB
Go
771 lines
16 KiB
Go
package rss
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
ext "github.com/mmcdole/gofeed/extensions"
|
|
"github.com/mmcdole/gofeed/internal/shared"
|
|
xpp "github.com/mmcdole/goxpp"
|
|
)
|
|
|
|
// Parser is a RSS Parser
|
|
type Parser struct {
|
|
base *shared.XMLBase
|
|
}
|
|
|
|
// Parse parses an xml feed into an rss.Feed
|
|
func (rp *Parser) Parse(feed io.Reader) (*Feed, error) {
|
|
p := xpp.NewXMLPullParser(feed, false, shared.NewReaderLabel)
|
|
rp.base = &shared.XMLBase{}
|
|
|
|
_, err := rp.base.FindRoot(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rp.parseRoot(p)
|
|
}
|
|
|
|
func (rp *Parser) parseRoot(p *xpp.XMLPullParser) (*Feed, error) {
|
|
rssErr := p.Expect(xpp.StartTag, "rss")
|
|
rdfErr := p.Expect(xpp.StartTag, "rdf")
|
|
if rssErr != nil && rdfErr != nil {
|
|
return nil, fmt.Errorf("%s or %s", rssErr.Error(), rdfErr.Error())
|
|
}
|
|
|
|
// Items found in feed root
|
|
var channel *Feed
|
|
var textinput *TextInput
|
|
var image *Image
|
|
items := []*Item{}
|
|
|
|
ver := rp.parseVersion(p)
|
|
|
|
for {
|
|
tok, err := rp.base.NextTag(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tok == xpp.EndTag {
|
|
break
|
|
}
|
|
|
|
if tok == xpp.StartTag {
|
|
|
|
// Skip any extensions found in the feed root.
|
|
if shared.IsExtension(p) {
|
|
p.Skip()
|
|
continue
|
|
}
|
|
|
|
name := strings.ToLower(p.Name)
|
|
|
|
if name == "channel" {
|
|
channel, err = rp.parseChannel(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if name == "item" {
|
|
item, err := rp.parseItem(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
} else if name == "textinput" {
|
|
textinput, err = rp.parseTextInput(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if name == "image" {
|
|
image, err = rp.parseImage(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
p.Skip()
|
|
}
|
|
}
|
|
}
|
|
|
|
rssErr = p.Expect(xpp.EndTag, "rss")
|
|
rdfErr = p.Expect(xpp.EndTag, "rdf")
|
|
if rssErr != nil && rdfErr != nil {
|
|
return nil, fmt.Errorf("%s or %s", rssErr.Error(), rdfErr.Error())
|
|
}
|
|
|
|
if channel == nil {
|
|
channel = &Feed{}
|
|
channel.Items = []*Item{}
|
|
}
|
|
|
|
if len(items) > 0 {
|
|
channel.Items = append(channel.Items, items...)
|
|
}
|
|
|
|
if textinput != nil {
|
|
channel.TextInput = textinput
|
|
}
|
|
|
|
if image != nil {
|
|
channel.Image = image
|
|
}
|
|
|
|
channel.Version = ver
|
|
return channel, nil
|
|
}
|
|
|
|
func (rp *Parser) parseChannel(p *xpp.XMLPullParser) (rss *Feed, err error) {
|
|
|
|
if err = p.Expect(xpp.StartTag, "channel"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rss = &Feed{}
|
|
rss.Items = []*Item{}
|
|
|
|
extensions := ext.Extensions{}
|
|
categories := []*Category{}
|
|
|
|
for {
|
|
tok, err := rp.base.NextTag(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tok == xpp.EndTag {
|
|
break
|
|
}
|
|
|
|
if tok == xpp.StartTag {
|
|
|
|
name := strings.ToLower(p.Name)
|
|
|
|
if shared.IsExtension(p) {
|
|
ext, err := shared.ParseExtension(extensions, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
extensions = ext
|
|
} else if name == "title" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Title = result
|
|
} else if name == "description" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Description = result
|
|
} else if name == "link" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Link = result
|
|
} else if name == "language" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Language = result
|
|
} else if name == "copyright" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Copyright = result
|
|
} else if name == "managingeditor" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.ManagingEditor = result
|
|
} else if name == "webmaster" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.WebMaster = result
|
|
} else if name == "pubdate" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.PubDate = result
|
|
date, err := shared.ParseDate(result)
|
|
if err == nil {
|
|
utcDate := date.UTC()
|
|
rss.PubDateParsed = &utcDate
|
|
}
|
|
} else if name == "lastbuilddate" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.LastBuildDate = result
|
|
date, err := shared.ParseDate(result)
|
|
if err == nil {
|
|
utcDate := date.UTC()
|
|
rss.LastBuildDateParsed = &utcDate
|
|
}
|
|
} else if name == "generator" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Generator = result
|
|
} else if name == "docs" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Docs = result
|
|
} else if name == "ttl" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.TTL = result
|
|
} else if name == "rating" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Rating = result
|
|
} else if name == "skiphours" {
|
|
result, err := rp.parseSkipHours(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.SkipHours = result
|
|
} else if name == "skipdays" {
|
|
result, err := rp.parseSkipDays(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.SkipDays = result
|
|
} else if name == "item" {
|
|
result, err := rp.parseItem(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Items = append(rss.Items, result)
|
|
} else if name == "cloud" {
|
|
result, err := rp.parseCloud(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Cloud = result
|
|
} else if name == "category" {
|
|
result, err := rp.parseCategory(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
categories = append(categories, result)
|
|
} else if name == "image" {
|
|
result, err := rp.parseImage(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.Image = result
|
|
} else if name == "textinput" {
|
|
result, err := rp.parseTextInput(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rss.TextInput = result
|
|
} else {
|
|
// Skip element as it isn't an extension and not
|
|
// part of the spec
|
|
p.Skip()
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = p.Expect(xpp.EndTag, "channel"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(categories) > 0 {
|
|
rss.Categories = categories
|
|
}
|
|
|
|
if len(extensions) > 0 {
|
|
rss.Extensions = extensions
|
|
|
|
if itunes, ok := rss.Extensions["itunes"]; ok {
|
|
rss.ITunesExt = ext.NewITunesFeedExtension(itunes)
|
|
}
|
|
|
|
if dc, ok := rss.Extensions["dc"]; ok {
|
|
rss.DublinCoreExt = ext.NewDublinCoreExtension(dc)
|
|
}
|
|
}
|
|
|
|
return rss, nil
|
|
}
|
|
|
|
func (rp *Parser) parseItem(p *xpp.XMLPullParser) (item *Item, err error) {
|
|
|
|
if err = p.Expect(xpp.StartTag, "item"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
item = &Item{}
|
|
extensions := ext.Extensions{}
|
|
categories := []*Category{}
|
|
|
|
for {
|
|
tok, err := rp.base.NextTag(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tok == xpp.EndTag {
|
|
break
|
|
}
|
|
|
|
if tok == xpp.StartTag {
|
|
|
|
name := strings.ToLower(p.Name)
|
|
|
|
if shared.IsExtension(p) {
|
|
ext, err := shared.ParseExtension(extensions, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Extensions = ext
|
|
} else if name == "title" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Title = result
|
|
} else if name == "description" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Description = result
|
|
} else if name == "encoded" {
|
|
space := strings.TrimSpace(p.Space)
|
|
if prefix, ok := p.Spaces[space]; ok && prefix == "content" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Content = result
|
|
}
|
|
} else if name == "link" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Link = result
|
|
} else if name == "author" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Author = result
|
|
} else if name == "comments" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Comments = result
|
|
} else if name == "pubdate" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.PubDate = result
|
|
date, err := shared.ParseDate(result)
|
|
if err == nil {
|
|
utcDate := date.UTC()
|
|
item.PubDateParsed = &utcDate
|
|
}
|
|
} else if name == "source" {
|
|
result, err := rp.parseSource(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Source = result
|
|
} else if name == "enclosure" {
|
|
result, err := rp.parseEnclosure(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Enclosure = result
|
|
} else if name == "guid" {
|
|
result, err := rp.parseGUID(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.GUID = result
|
|
} else if name == "category" {
|
|
result, err := rp.parseCategory(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
categories = append(categories, result)
|
|
} else {
|
|
// Skip any elements not part of the item spec
|
|
p.Skip()
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(categories) > 0 {
|
|
item.Categories = categories
|
|
}
|
|
|
|
if len(extensions) > 0 {
|
|
item.Extensions = extensions
|
|
|
|
if itunes, ok := item.Extensions["itunes"]; ok {
|
|
item.ITunesExt = ext.NewITunesItemExtension(itunes)
|
|
}
|
|
|
|
if dc, ok := item.Extensions["dc"]; ok {
|
|
item.DublinCoreExt = ext.NewDublinCoreExtension(dc)
|
|
}
|
|
}
|
|
|
|
if err = p.Expect(xpp.EndTag, "item"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
|
|
func (rp *Parser) parseSource(p *xpp.XMLPullParser) (source *Source, err error) {
|
|
if err = p.Expect(xpp.StartTag, "source"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
source = &Source{}
|
|
source.URL = p.Attribute("url")
|
|
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return source, err
|
|
}
|
|
source.Title = result
|
|
|
|
if err = p.Expect(xpp.EndTag, "source"); err != nil {
|
|
return nil, err
|
|
}
|
|
return source, nil
|
|
}
|
|
|
|
func (rp *Parser) parseEnclosure(p *xpp.XMLPullParser) (enclosure *Enclosure, err error) {
|
|
if err = p.Expect(xpp.StartTag, "enclosure"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
enclosure = &Enclosure{}
|
|
enclosure.URL = p.Attribute("url")
|
|
enclosure.Length = p.Attribute("length")
|
|
enclosure.Type = p.Attribute("type")
|
|
|
|
// Ignore any enclosure text
|
|
_, err = p.NextText()
|
|
if err != nil {
|
|
return enclosure, err
|
|
}
|
|
|
|
if err = p.Expect(xpp.EndTag, "enclosure"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return enclosure, nil
|
|
}
|
|
|
|
func (rp *Parser) parseImage(p *xpp.XMLPullParser) (image *Image, err error) {
|
|
if err = p.Expect(xpp.StartTag, "image"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
image = &Image{}
|
|
|
|
for {
|
|
tok, err := rp.base.NextTag(p)
|
|
if err != nil {
|
|
return image, err
|
|
}
|
|
|
|
if tok == xpp.EndTag {
|
|
break
|
|
}
|
|
|
|
if tok == xpp.StartTag {
|
|
name := strings.ToLower(p.Name)
|
|
|
|
if name == "url" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
image.URL = result
|
|
} else if name == "title" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
image.Title = result
|
|
} else if name == "link" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
image.Link = result
|
|
} else if name == "width" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
image.Width = result
|
|
} else if name == "height" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
image.Height = result
|
|
} else if name == "description" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
image.Description = result
|
|
} else {
|
|
p.Skip()
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = p.Expect(xpp.EndTag, "image"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return image, nil
|
|
}
|
|
|
|
func (rp *Parser) parseGUID(p *xpp.XMLPullParser) (guid *GUID, err error) {
|
|
if err = p.Expect(xpp.StartTag, "guid"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
guid = &GUID{}
|
|
guid.IsPermalink = p.Attribute("isPermalink")
|
|
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return
|
|
}
|
|
guid.Value = result
|
|
|
|
if err = p.Expect(xpp.EndTag, "guid"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return guid, nil
|
|
}
|
|
|
|
func (rp *Parser) parseCategory(p *xpp.XMLPullParser) (cat *Category, err error) {
|
|
|
|
if err = p.Expect(xpp.StartTag, "category"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cat = &Category{}
|
|
cat.Domain = p.Attribute("domain")
|
|
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cat.Value = result
|
|
|
|
if err = p.Expect(xpp.EndTag, "category"); err != nil {
|
|
return nil, err
|
|
}
|
|
return cat, nil
|
|
}
|
|
|
|
func (rp *Parser) parseTextInput(p *xpp.XMLPullParser) (*TextInput, error) {
|
|
if err := p.Expect(xpp.StartTag, "textinput"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ti := &TextInput{}
|
|
|
|
for {
|
|
tok, err := rp.base.NextTag(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tok == xpp.EndTag {
|
|
break
|
|
}
|
|
|
|
if tok == xpp.StartTag {
|
|
name := strings.ToLower(p.Name)
|
|
|
|
if name == "title" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ti.Title = result
|
|
} else if name == "description" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ti.Description = result
|
|
} else if name == "name" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ti.Name = result
|
|
} else if name == "link" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ti.Link = result
|
|
} else {
|
|
p.Skip()
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := p.Expect(xpp.EndTag, "textinput"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ti, nil
|
|
}
|
|
|
|
func (rp *Parser) parseSkipHours(p *xpp.XMLPullParser) ([]string, error) {
|
|
if err := p.Expect(xpp.StartTag, "skiphours"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hours := []string{}
|
|
|
|
for {
|
|
tok, err := rp.base.NextTag(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tok == xpp.EndTag {
|
|
break
|
|
}
|
|
|
|
if tok == xpp.StartTag {
|
|
name := strings.ToLower(p.Name)
|
|
if name == "hour" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hours = append(hours, result)
|
|
} else {
|
|
p.Skip()
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := p.Expect(xpp.EndTag, "skiphours"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return hours, nil
|
|
}
|
|
|
|
func (rp *Parser) parseSkipDays(p *xpp.XMLPullParser) ([]string, error) {
|
|
if err := p.Expect(xpp.StartTag, "skipdays"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
days := []string{}
|
|
|
|
for {
|
|
tok, err := rp.base.NextTag(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tok == xpp.EndTag {
|
|
break
|
|
}
|
|
|
|
if tok == xpp.StartTag {
|
|
name := strings.ToLower(p.Name)
|
|
if name == "day" {
|
|
result, err := shared.ParseText(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
days = append(days, result)
|
|
} else {
|
|
p.Skip()
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := p.Expect(xpp.EndTag, "skipdays"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return days, nil
|
|
}
|
|
|
|
func (rp *Parser) parseCloud(p *xpp.XMLPullParser) (*Cloud, error) {
|
|
if err := p.Expect(xpp.StartTag, "cloud"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cloud := &Cloud{}
|
|
cloud.Domain = p.Attribute("domain")
|
|
cloud.Port = p.Attribute("port")
|
|
cloud.Path = p.Attribute("path")
|
|
cloud.RegisterProcedure = p.Attribute("registerProcedure")
|
|
cloud.Protocol = p.Attribute("protocol")
|
|
|
|
rp.base.NextTag(p)
|
|
|
|
if err := p.Expect(xpp.EndTag, "cloud"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cloud, nil
|
|
}
|
|
|
|
func (rp *Parser) parseVersion(p *xpp.XMLPullParser) (ver string) {
|
|
name := strings.ToLower(p.Name)
|
|
if name == "rss" {
|
|
ver = p.Attribute("version")
|
|
} else if name == "rdf" {
|
|
ns := p.Attribute("xmlns")
|
|
if ns == "http://channel.netscape.com/rdf/simple/0.9/" ||
|
|
ns == "http://my.netscape.com/rdf/simple/0.9/" {
|
|
ver = "0.9"
|
|
} else if ns == "http://purl.org/rss/1.0/" {
|
|
ver = "1.0"
|
|
}
|
|
}
|
|
return
|
|
}
|