feed-normalizer 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,137 +1,138 @@
1
- require 'simple-rss'
2
-
3
- # Monkey patches for outstanding issues logged in the simple-rss project.
4
- # * Add support for issued time field:
5
- # http://rubyforge.org/tracker/index.php?func=detail&aid=13980&group_id=893&atid=3517
6
- # * The '+' symbol is lost when escaping fields.
7
- # http://rubyforge.org/tracker/index.php?func=detail&aid=10852&group_id=893&atid=3517
8
- #
9
- class SimpleRSS
10
- @@item_tags << :issued
11
-
12
- undef clean_content
13
- def clean_content(tag, attrs, content)
14
- content = content.to_s
15
- case tag
16
- when :pubDate, :lastBuildDate, :published, :updated, :expirationDate, :modified, :'dc:date', :issued
17
- Time.parse(content) rescue unescape(content)
18
- when :author, :contributor, :skipHours, :skipDays
19
- unescape(content.gsub(/<.*?>/,''))
20
- else
21
- content.empty? && "#{attrs} " =~ /href=['"]?([^'"]*)['" ]/mi ? $1.strip : unescape(content)
22
- end
23
- end
24
-
25
- undef unescape
26
- def unescape(s)
27
- if s =~ /^(<!\[CDATA\[|\]\]>)/
28
- # Raw HTML is inside the CDATA, so just remove the CDATA wrapper.
29
- s.gsub(/(<!\[CDATA\[|\]\]>)/,'').strip
30
- elsif s =~ /[<>]/
31
- # Already looks like HTML.
32
- s
33
- else
34
- # Make it HTML.
35
- FeedNormalizer::HtmlCleaner.unescapeHTML(s)
36
- end
37
- end
38
- end
39
-
40
- module FeedNormalizer
41
-
42
- # The SimpleRSS parser can handle both RSS and Atom feeds.
43
- class SimpleRssParser < Parser
44
-
45
- def self.parser
46
- SimpleRSS
47
- end
48
-
49
- def self.parse(xml, loose)
50
- begin
51
- atomrss = parser.parse(xml)
52
- rescue Exception => e
53
- #puts "Parser #{parser} failed because #{e.message.gsub("\n",', ')}"
54
- return nil
55
- end
56
-
57
- package(atomrss)
58
- end
59
-
60
- # Fairly low priority; a slower, liberal parser.
61
- def self.priority
62
- 900
63
- end
64
-
65
- protected
66
-
67
- def self.package(atomrss)
68
- feed = Feed.new(self)
69
-
70
- # root elements
71
- feed_mapping = {
72
- :generator => :generator,
73
- :title => :title,
74
- :last_updated => [:updated, :lastBuildDate, :pubDate, :dc_date],
75
- :copyright => [:copyright, :rights],
76
- :authors => [:author, :webMaster, :managingEditor, :contributor],
77
- :urls => :link,
78
- :description => [:description, :subtitle],
79
- :ttl => :ttl
80
- }
81
-
82
- map_functions!(feed_mapping, atomrss, feed)
83
-
84
- # custom channel elements
85
- feed.id = feed_id(atomrss)
86
- feed.image = image(atomrss)
87
-
88
-
89
- # entry elements
90
- entry_mapping = {
91
- :date_published => [:pubDate, :published, :dc_date, :issued],
92
- :urls => :link,
93
- :description => [:description, :summary],
94
- :content => [:content, :content_encoded, :description],
95
- :title => :title,
96
- :authors => [:author, :contributor, :dc_creator],
97
- :categories => :category,
98
- :last_updated => [:updated, :dc_date, :pubDate]
99
- }
100
-
101
- atomrss.entries.each do |atomrss_entry|
102
- feed_entry = Entry.new
103
- map_functions!(entry_mapping, atomrss_entry, feed_entry)
104
-
105
- # custom entry elements
106
- feed_entry.id = atomrss_entry.guid || atomrss_entry[:id] # entries are a Hash..
107
- feed_entry.copyright = atomrss_entry.copyright || (atomrss.respond_to?(:copyright) ? atomrss.copyright : nil)
108
-
109
- feed.entries << feed_entry
110
- end
111
-
112
- feed
113
- end
114
-
115
- def self.image(parser)
116
- if parser.respond_to?(:image) && parser.image
117
- if parser.image =~ /<url>/ # RSS image contains an <url> spec
118
- parser.image.scan(/<url>(.*?)<\/url>/).to_s
119
- else
120
- parser.image # Atom contains just the url
121
- end
122
- elsif parser.respond_to?(:logo) && parser.logo
123
- parser.logo
124
- end
125
- end
126
-
127
- def self.feed_id(parser)
128
- overridden_value(parser, :id) || ("#{parser.link}" if parser.respond_to?(:link))
129
- end
130
-
131
- # gets the value returned from the method if it overriden, otherwise nil.
132
- def self.overridden_value(object, method)
133
- object.class.public_instance_methods(false).include? method
134
- end
135
-
136
- end
137
- end
1
+ require 'simple-rss'
2
+
3
+ # Monkey patches for outstanding issues logged in the simple-rss project.
4
+ # * Add support for issued time field:
5
+ # http://rubyforge.org/tracker/index.php?func=detail&aid=13980&group_id=893&atid=3517
6
+ # * The '+' symbol is lost when escaping fields.
7
+ # http://rubyforge.org/tracker/index.php?func=detail&aid=10852&group_id=893&atid=3517
8
+ #
9
+ class SimpleRSS
10
+ @@item_tags << :issued
11
+
12
+ undef clean_content
13
+ def clean_content(tag, attrs, content)
14
+ content = content.to_s
15
+ case tag
16
+ when :pubDate, :lastBuildDate, :published, :updated, :expirationDate, :modified, :'dc:date', :issued
17
+ Time.parse(content) rescue unescape(content)
18
+ when :author, :contributor, :skipHours, :skipDays
19
+ unescape(content.gsub(/<.*?>/,''))
20
+ else
21
+ content.empty? && "#{attrs} " =~ /href=['"]?([^'"]*)['" ]/mi ? $1.strip : unescape(content)
22
+ end
23
+ end
24
+
25
+ undef unescape
26
+ def unescape(s)
27
+ if s =~ /^\s*(<!\[CDATA\[|\]\]>)/
28
+ # Raw HTML is inside the CDATA, so just remove the CDATA wrapper.
29
+ s.gsub(/(<!\[CDATA\[|\]\]>)/,'')
30
+ elsif s =~ /[<>]/
31
+ # Already looks like HTML.
32
+ s
33
+ else
34
+ # Make it HTML.
35
+ FeedNormalizer::HtmlCleaner.unescapeHTML(s)
36
+ end
37
+ end
38
+ end
39
+
40
+ module FeedNormalizer
41
+
42
+ # The SimpleRSS parser can handle both RSS and Atom feeds.
43
+ class SimpleRssParser < Parser
44
+
45
+ def self.parser
46
+ SimpleRSS
47
+ end
48
+
49
+ def self.parse(xml, loose)
50
+ begin
51
+ atomrss = parser.parse(xml)
52
+ rescue Exception => e
53
+ #puts "Parser #{parser} failed because #{e.message.gsub("\n",', ')}"
54
+ return nil
55
+ end
56
+
57
+ package(atomrss)
58
+ end
59
+
60
+ # Fairly low priority; a slower, liberal parser.
61
+ def self.priority
62
+ 900
63
+ end
64
+
65
+ protected
66
+
67
+ def self.package(atomrss)
68
+ feed = Feed.new(self)
69
+
70
+ # root elements
71
+ feed_mapping = {
72
+ :generator => :generator,
73
+ :title => :title,
74
+ :last_updated => [:updated, :lastBuildDate, :pubDate, :dc_date],
75
+ :copyright => [:copyright, :rights],
76
+ :authors => [:author, :webMaster, :managingEditor, :contributor],
77
+ :urls => :link,
78
+ :description => [:description, :subtitle],
79
+ :ttl => :ttl
80
+ }
81
+
82
+ map_functions!(feed_mapping, atomrss, feed)
83
+
84
+ # custom channel elements
85
+ feed.id = feed_id(atomrss)
86
+ feed.image = image(atomrss)
87
+
88
+
89
+ # entry elements
90
+ entry_mapping = {
91
+ :date_published => [:pubDate, :published, :dc_date, :issued],
92
+ :urls => :link,
93
+ :enclosures => :enclosure,
94
+ :description => [:description, :summary],
95
+ :content => [:content, :content_encoded, :description],
96
+ :title => :title,
97
+ :authors => [:author, :contributor, :dc_creator],
98
+ :categories => :category,
99
+ :last_updated => [:updated, :dc_date, :pubDate]
100
+ }
101
+
102
+ atomrss.entries.each do |atomrss_entry|
103
+ feed_entry = Entry.new
104
+ map_functions!(entry_mapping, atomrss_entry, feed_entry)
105
+
106
+ # custom entry elements
107
+ feed_entry.id = atomrss_entry.guid || atomrss_entry[:id] # entries are a Hash..
108
+ feed_entry.copyright = atomrss_entry.copyright || (atomrss.respond_to?(:copyright) ? atomrss.copyright : nil)
109
+
110
+ feed.entries << feed_entry
111
+ end
112
+
113
+ feed
114
+ end
115
+
116
+ def self.image(parser)
117
+ if parser.respond_to?(:image) && parser.image
118
+ if parser.image =~ /<url>/ # RSS image contains an <url> spec
119
+ parser.image.scan(/<url>(.*?)<\/url>/).to_s
120
+ else
121
+ parser.image # Atom contains just the url
122
+ end
123
+ elsif parser.respond_to?(:logo) && parser.logo
124
+ parser.logo
125
+ end
126
+ end
127
+
128
+ def self.feed_id(parser)
129
+ overridden_value(parser, :id) || ("#{parser.link}" if parser.respond_to?(:link))
130
+ end
131
+
132
+ # gets the value returned from the method if it overriden, otherwise nil.
133
+ def self.overridden_value(object, method)
134
+ object.class.public_instance_methods(false).include? method
135
+ end
136
+
137
+ end
138
+ end
@@ -1,244 +1,245 @@
1
-
2
- module FeedNormalizer
3
-
4
- module Singular
5
-
6
- # If the method being called is a singular (in this simple case, does not
7
- # end with an 's'), then it calls the plural method, and calls the first
8
- # element. We're assuming that plural methods provide an array.
9
- #
10
- # Example:
11
- # Object contains an array called 'alphas', which looks like [:a, :b, :c].
12
- # Call object.alpha and :a is returned.
13
- def method_missing(name, *args)
14
- return self.send(:"#{name}s").first rescue super(name, *args)
15
- end
16
-
17
- def respond_to?(x, y=false)
18
- self.class::ELEMENTS.include?(x) || self.class::ELEMENTS.include?(:"#{x}s") || super(x, y)
19
- end
20
-
21
- end
22
-
23
- module ElementEquality
24
-
25
- def eql?(other)
26
- self == (other)
27
- end
28
-
29
- def ==(other)
30
- other.equal?(self) ||
31
- (other.instance_of?(self.class) &&
32
- self.class::ELEMENTS.all?{ |el| self.send(el) == other.send(el)} )
33
- end
34
-
35
- # Returns the difference between two Feed instances as a hash.
36
- # Any top-level differences in the Feed object as presented as:
37
- #
38
- # { :title => [content, other_content] }
39
- #
40
- # For differences at the items level, an array of hashes shows the diffs
41
- # on a per-entry basis. Only entries that differ will contain a hash:
42
- #
43
- # { :items => [
44
- # {:title => ["An article tile", "A new article title"]},
45
- # {:title => ["one title", "a different title"]} ]}
46
- #
47
- # If the number of items in each feed are different, then the count of each
48
- # is provided instead:
49
- #
50
- # { :items => [4,5] }
51
- #
52
- # This method can also be useful for human-readable feed comparison if
53
- # its output is dumped to YAML.
54
- def diff(other, elements = self.class::ELEMENTS)
55
- diffs = {}
56
-
57
- elements.each do |element|
58
- if other.respond_to?(element)
59
- self_value = self.send(element)
60
- other_value = other.send(element)
61
-
62
- next if self_value == other_value
63
-
64
- diffs[element] = if other_value.respond_to?(:diff)
65
- self_value.diff(other_value)
66
-
67
- elsif other_value.is_a?(Enumerable) && other_value.all?{|v| v.respond_to?(:diff)}
68
-
69
- if self_value.size != other_value.size
70
- [self_value.size, other_value.size]
71
- else
72
- enum_diffs = []
73
- self_value.each_with_index do |val, index|
74
- enum_diffs << val.diff(other_value[index], val.class::ELEMENTS)
75
- end
76
- enum_diffs.reject{|h| h.empty?}
77
- end
78
-
79
- else
80
- [other_value, self_value] unless other_value == self_value
81
- end
82
- end
83
- end
84
-
85
- diffs
86
- end
87
-
88
- end
89
-
90
- module ElementCleaner
91
- # Recursively cleans all elements in place.
92
- #
93
- # Only allow tags in whitelist. Always parse the html with a parser and delete
94
- # all tags that arent on the list.
95
- #
96
- # For feed elements that can contain HTML:
97
- # - feed.(title|description)
98
- # - feed.entries[n].(title|description|content)
99
- #
100
- def clean!
101
- self.class::SIMPLE_ELEMENTS.each do |element|
102
- val = self.send(element)
103
-
104
- send("#{element}=", (val.is_a?(Array) ?
105
- val.collect{|v| HtmlCleaner.flatten(v.to_s)} : HtmlCleaner.flatten(val.to_s)))
106
- end
107
-
108
- self.class::HTML_ELEMENTS.each do |element|
109
- send("#{element}=", HtmlCleaner.clean(self.send(element).to_s))
110
- end
111
-
112
- self.class::BLENDED_ELEMENTS.each do |element|
113
- self.send(element).collect{|v| v.clean!}
114
- end
115
- end
116
- end
117
-
118
- module TimeFix
119
- # Reparse any Time instances, due to RSS::Parser's redefinition of
120
- # certain aspects of the Time class that creates unexpected behaviour
121
- # when extending the Time class, as some common third party libraries do.
122
- # See http://code.google.com/p/feed-normalizer/issues/detail?id=13.
123
- def reparse(obj)
124
- @parsed ||= false
125
-
126
- return obj if @parsed
127
-
128
- if obj.is_a?(Time)
129
- @parsed = true
130
- Time.at(obj) rescue obj
131
- end
132
- end
133
- end
134
-
135
- module RewriteRelativeLinks
136
- def rewrite_relative_links(text, url)
137
- if host = url_host(url)
138
- text.to_s.gsub(/(href|src)=('|")\//, '\1=\2http://' + host + '/')
139
- else
140
- text
141
- end
142
- end
143
-
144
- private
145
- def url_host(url)
146
- URI.parse(url).host rescue nil
147
- end
148
- end
149
-
150
-
151
- # Represents a feed item entry.
152
- # Available fields are:
153
- # * content
154
- # * description
155
- # * title
156
- # * date_published
157
- # * urls / url
158
- # * id
159
- # * authors / author
160
- # * copyright
161
- # * categories
162
- class Entry
163
- include Singular, ElementEquality, ElementCleaner, TimeFix, RewriteRelativeLinks
164
-
165
- HTML_ELEMENTS = [:content, :description, :title]
166
- SIMPLE_ELEMENTS = [:date_published, :urls, :id, :authors, :copyright, :categories, :last_updated]
167
- BLENDED_ELEMENTS = []
168
-
169
- ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
170
-
171
- attr_accessor(*ELEMENTS)
172
-
173
- def initialize
174
- @urls = []
175
- @authors = []
176
- @categories = []
177
- @date_published, @content = nil
178
- end
179
-
180
- undef date_published
181
- def date_published
182
- @date_published = reparse(@date_published)
183
- end
184
-
185
- undef content
186
- def content
187
- @content = rewrite_relative_links(@content, url)
188
- end
189
-
190
- end
191
-
192
- # Represents the root element of a feed.
193
- # Available fields are:
194
- # * title
195
- # * description
196
- # * id
197
- # * last_updated
198
- # * copyright
199
- # * authors / author
200
- # * urls / url
201
- # * image
202
- # * generator
203
- # * items / channel
204
- class Feed
205
- include Singular, ElementEquality, ElementCleaner, TimeFix
206
-
207
- # Elements that can contain HTML fragments.
208
- HTML_ELEMENTS = [:title, :description]
209
-
210
- # Elements that contain 'plain' Strings, with HTML escaped.
211
- SIMPLE_ELEMENTS = [:id, :last_updated, :copyright, :authors, :urls, :image, :generator, :ttl, :skip_hours, :skip_days]
212
-
213
- # Elements that contain both HTML and escaped HTML.
214
- BLENDED_ELEMENTS = [:items]
215
-
216
- ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
217
-
218
- attr_accessor(*ELEMENTS)
219
- attr_accessor(:parser)
220
-
221
- alias :entries :items
222
-
223
- def initialize(wrapper)
224
- # set up associations (i.e. arrays where needed)
225
- @urls = []
226
- @authors = []
227
- @skip_hours = []
228
- @skip_days = []
229
- @items = []
230
- @parser = wrapper.parser.to_s
231
- @last_updated = nil
232
- end
233
-
234
- undef last_updated
235
- def last_updated
236
- @last_updated = reparse(@last_updated)
237
- end
238
-
239
- def channel() self end
240
-
241
- end
242
-
243
- end
244
-
1
+
2
+ module FeedNormalizer
3
+
4
+ module Singular
5
+
6
+ # If the method being called is a singular (in this simple case, does not
7
+ # end with an 's'), then it calls the plural method, and calls the first
8
+ # element. We're assuming that plural methods provide an array.
9
+ #
10
+ # Example:
11
+ # Object contains an array called 'alphas', which looks like [:a, :b, :c].
12
+ # Call object.alpha and :a is returned.
13
+ def method_missing(name, *args)
14
+ return self.send(:"#{name}s").first rescue super(name, *args)
15
+ end
16
+
17
+ def respond_to?(x, y=false)
18
+ self.class::ELEMENTS.include?(x) || self.class::ELEMENTS.include?(:"#{x}s") || super(x, y)
19
+ end
20
+
21
+ end
22
+
23
+ module ElementEquality
24
+
25
+ def eql?(other)
26
+ self == (other)
27
+ end
28
+
29
+ def ==(other)
30
+ other.equal?(self) ||
31
+ (other.instance_of?(self.class) &&
32
+ self.class::ELEMENTS.all?{ |el| self.send(el) == other.send(el)} )
33
+ end
34
+
35
+ # Returns the difference between two Feed instances as a hash.
36
+ # Any top-level differences in the Feed object as presented as:
37
+ #
38
+ # { :title => [content, other_content] }
39
+ #
40
+ # For differences at the items level, an array of hashes shows the diffs
41
+ # on a per-entry basis. Only entries that differ will contain a hash:
42
+ #
43
+ # { :items => [
44
+ # {:title => ["An article tile", "A new article title"]},
45
+ # {:title => ["one title", "a different title"]} ]}
46
+ #
47
+ # If the number of items in each feed are different, then the count of each
48
+ # is provided instead:
49
+ #
50
+ # { :items => [4,5] }
51
+ #
52
+ # This method can also be useful for human-readable feed comparison if
53
+ # its output is dumped to YAML.
54
+ def diff(other, elements = self.class::ELEMENTS)
55
+ diffs = {}
56
+
57
+ elements.each do |element|
58
+ if other.respond_to?(element)
59
+ self_value = self.send(element)
60
+ other_value = other.send(element)
61
+
62
+ next if self_value == other_value
63
+
64
+ diffs[element] = if other_value.respond_to?(:diff)
65
+ self_value.diff(other_value)
66
+
67
+ elsif other_value.is_a?(Enumerable) && other_value.all?{|v| v.respond_to?(:diff)}
68
+
69
+ if self_value.size != other_value.size
70
+ [self_value.size, other_value.size]
71
+ else
72
+ enum_diffs = []
73
+ self_value.each_with_index do |val, index|
74
+ enum_diffs << val.diff(other_value[index], val.class::ELEMENTS)
75
+ end
76
+ enum_diffs.reject{|h| h.empty?}
77
+ end
78
+
79
+ else
80
+ [other_value, self_value] unless other_value == self_value
81
+ end
82
+ end
83
+ end
84
+
85
+ diffs
86
+ end
87
+
88
+ end
89
+
90
+ module ElementCleaner
91
+ # Recursively cleans all elements in place.
92
+ #
93
+ # Only allow tags in whitelist. Always parse the html with a parser and delete
94
+ # all tags that arent on the list.
95
+ #
96
+ # For feed elements that can contain HTML:
97
+ # - feed.(title|description)
98
+ # - feed.entries[n].(title|description|content)
99
+ #
100
+ def clean!
101
+ self.class::SIMPLE_ELEMENTS.each do |element|
102
+ val = self.send(element)
103
+
104
+ send("#{element}=", (val.is_a?(Array) ?
105
+ val.collect{|v| HtmlCleaner.flatten(v.to_s)} : HtmlCleaner.flatten(val.to_s)))
106
+ end
107
+
108
+ self.class::HTML_ELEMENTS.each do |element|
109
+ send("#{element}=", HtmlCleaner.clean(self.send(element).to_s))
110
+ end
111
+
112
+ self.class::BLENDED_ELEMENTS.each do |element|
113
+ self.send(element).collect{|v| v.clean!}
114
+ end
115
+ end
116
+ end
117
+
118
+ module TimeFix
119
+ # Reparse any Time instances, due to RSS::Parser's redefinition of
120
+ # certain aspects of the Time class that creates unexpected behaviour
121
+ # when extending the Time class, as some common third party libraries do.
122
+ # See http://code.google.com/p/feed-normalizer/issues/detail?id=13.
123
+ def reparse(obj)
124
+ @parsed ||= false
125
+
126
+ return obj if @parsed
127
+
128
+ if obj.is_a?(Time)
129
+ @parsed = true
130
+ Time.at(obj) rescue obj
131
+ end
132
+ end
133
+ end
134
+
135
+ module RewriteRelativeLinks
136
+ def rewrite_relative_links(text, url)
137
+ if host = url_host(url)
138
+ text.to_s.gsub(/(href|src)=('|")\//, '\1=\2http://' + host + '/')
139
+ else
140
+ text
141
+ end
142
+ end
143
+
144
+ private
145
+ def url_host(url)
146
+ URI.parse(url).host rescue nil
147
+ end
148
+ end
149
+
150
+
151
+ # Represents a feed item entry.
152
+ # Available fields are:
153
+ # * content
154
+ # * description
155
+ # * title
156
+ # * date_published
157
+ # * urls / url
158
+ # * id
159
+ # * authors / author
160
+ # * copyright
161
+ # * categories
162
+ class Entry
163
+ include Singular, ElementEquality, ElementCleaner, TimeFix, RewriteRelativeLinks
164
+
165
+ HTML_ELEMENTS = [:content, :description, :title]
166
+ SIMPLE_ELEMENTS = [:date_published, :urls, :id, :authors, :copyright, :categories, :last_updated, :enclosures]
167
+ BLENDED_ELEMENTS = []
168
+
169
+ ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
170
+
171
+ attr_accessor(*ELEMENTS)
172
+
173
+ def initialize
174
+ @urls = []
175
+ @authors = []
176
+ @categories = []
177
+ @enclosures = []
178
+ @date_published, @content = nil
179
+ end
180
+
181
+ undef date_published
182
+ def date_published
183
+ @date_published = reparse(@date_published)
184
+ end
185
+
186
+ undef content
187
+ def content
188
+ @content = rewrite_relative_links(@content, url)
189
+ end
190
+
191
+ end
192
+
193
+ # Represents the root element of a feed.
194
+ # Available fields are:
195
+ # * title
196
+ # * description
197
+ # * id
198
+ # * last_updated
199
+ # * copyright
200
+ # * authors / author
201
+ # * urls / url
202
+ # * image
203
+ # * generator
204
+ # * items / channel
205
+ class Feed
206
+ include Singular, ElementEquality, ElementCleaner, TimeFix
207
+
208
+ # Elements that can contain HTML fragments.
209
+ HTML_ELEMENTS = [:title, :description]
210
+
211
+ # Elements that contain 'plain' Strings, with HTML escaped.
212
+ SIMPLE_ELEMENTS = [:id, :last_updated, :copyright, :authors, :urls, :image, :generator, :ttl, :skip_hours, :skip_days]
213
+
214
+ # Elements that contain both HTML and escaped HTML.
215
+ BLENDED_ELEMENTS = [:items]
216
+
217
+ ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
218
+
219
+ attr_accessor(*ELEMENTS)
220
+ attr_accessor(:parser)
221
+
222
+ alias :entries :items
223
+
224
+ def initialize(wrapper)
225
+ # set up associations (i.e. arrays where needed)
226
+ @urls = []
227
+ @authors = []
228
+ @skip_hours = []
229
+ @skip_days = []
230
+ @items = []
231
+ @parser = wrapper.parser.to_s
232
+ @last_updated = nil
233
+ end
234
+
235
+ undef last_updated
236
+ def last_updated
237
+ @last_updated = reparse(@last_updated)
238
+ end
239
+
240
+ def channel() self end
241
+
242
+ end
243
+
244
+ end
245
+