-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathstructures.rb
More file actions
262 lines (213 loc) · 6.77 KB
/
Copy pathstructures.rb
File metadata and controls
262 lines (213 loc) · 6.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
module FeedNormalizer
module Singular
# If the method being called is a singular (in this simple case, does not
# end with an 's'), then it calls the plural method, and calls the first
# element. We're assuming that plural methods provide an array.
#
# Example:
# Object contains an array called 'alphas', which looks like [:a, :b, :c].
# Call object.alpha and :a is returned.
def method_missing(name, *args)
plural_name = :"#{name}s"
return self.send(plural_name).first if respond_to?(plural_name)
super(name, *args)
end
def respond_to?(x, y=false)
self.class::ELEMENTS.include?(x) || self.class::ELEMENTS.include?(:"#{x}s") || super(x, y)
end
end
module ElementEquality
def eql?(other)
self == (other)
end
def ==(other)
other.equal?(self) ||
(other.instance_of?(self.class) &&
self.class::ELEMENTS.all?{ |el| self.send(el) == other.send(el)} )
end
# Returns the difference between two Feed instances as a hash.
# Any top-level differences in the Feed object as presented as:
#
# { :title => [content, other_content] }
#
# For differences at the items level, an array of hashes shows the diffs
# on a per-entry basis. Only entries that differ will contain a hash:
#
# { :items => [
# {:title => ["An article tile", "A new article title"]},
# {:title => ["one title", "a different title"]} ]}
#
# If the number of items in each feed are different, then the count of each
# is provided instead:
#
# { :items => [4,5] }
#
# This method can also be useful for human-readable feed comparison if
# its output is dumped to YAML.
def diff(other, elements = self.class::ELEMENTS)
diffs = {}
elements.each do |element|
if other.respond_to?(element)
self_value = self.send(element)
other_value = other.send(element)
next if self_value == other_value
diffs[element] = if other_value.respond_to?(:diff)
self_value.diff(other_value)
elsif other_value.is_a?(Enumerable) && other_value.all?{|v| v.respond_to?(:diff)}
if self_value.size != other_value.size
[self_value.size, other_value.size]
else
enum_diffs = []
self_value.each_with_index do |val, index|
enum_diffs << val.diff(other_value[index], val.class::ELEMENTS)
end
enum_diffs.reject{|h| h.empty?}
end
else
[other_value, self_value] unless other_value == self_value
end
end
end
diffs
end
end
module ElementCleaner
# Recursively cleans all elements in place.
#
# Only allow tags in whitelist. Always parse the html with a parser and delete
# all tags that arent on the list.
#
# For feed elements that can contain HTML:
# - feed.(title|description)
# - feed.entries[n].(title|description|content)
#
def clean!
self.class::SIMPLE_ELEMENTS.each do |element|
val = self.send(element)
send("#{element}=", (val.is_a?(Array) ?
val.collect{|v| HtmlCleaner.flatten(v.to_s)} : HtmlCleaner.flatten(val.to_s)))
end
self.class::HTML_ELEMENTS.each do |element|
send("#{element}=", HtmlCleaner.clean(self.send(element).to_s))
end
self.class::BLENDED_ELEMENTS.each do |element|
self.send(element).collect{|v| v.clean!}
end
end
end
module TimeFix
# Reparse any Time instances, due to RSS::Parser's redefinition of
# certain aspects of the Time class that creates unexpected behaviour
# when extending the Time class, as some common third party libraries do.
# See http://code.google.com/p/feed-normalizer/issues/detail?id=13.
def reparse(obj)
@parsed ||= false
if obj.is_a?(String)
@parsed = true
begin
Time.at(obj) rescue Time.rfc2822(obj) rescue Time.parse(obj)
rescue
@parsed = false
obj
end
else
return obj if @parsed
if obj.is_a?(Time)
@parsed = true
Time.at(obj) rescue obj
end
end
end
end
module RewriteRelativeLinks
def rewrite_relative_links(text, url)
if host = url_host(url)
text.to_s.gsub(/(href|src)=('|")\//, '\1=\2http://' + host + '/')
else
text
end
end
private
def url_host(url)
URI.parse(url).host rescue nil
end
end
# Represents a feed item entry.
# Available fields are:
# * content
# * description
# * title
# * date_published
# * urls / url
# * id
# * authors / author
# * copyright
# * categories
class Entry
include Singular, ElementEquality, ElementCleaner, TimeFix, RewriteRelativeLinks
HTML_ELEMENTS = [:content, :description, :title]
SIMPLE_ELEMENTS = [:date_published, :urls, :id, :authors, :copyright, :categories, :last_updated, :enclosures]
BLENDED_ELEMENTS = []
ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
attr_accessor(*ELEMENTS)
def initialize
@urls = []
@authors = []
@categories = []
@enclosures = []
@date_published, @content, @last_updated = nil
end
undef date_published
def date_published
@date_published = reparse(@date_published)
end
undef last_updated
def last_updated
@last_updated = reparse(@last_updated)
end
undef content
def content
@content = rewrite_relative_links(@content, url)
end
end
# Represents the root element of a feed.
# Available fields are:
# * title
# * description
# * id
# * last_updated
# * copyright
# * authors / author
# * urls / url
# * image
# * generator
# * items / channel
class Feed
include Singular, ElementEquality, ElementCleaner, TimeFix
# Elements that can contain HTML fragments.
HTML_ELEMENTS = [:title, :description]
# Elements that contain 'plain' Strings, with HTML escaped.
SIMPLE_ELEMENTS = [:id, :last_updated, :copyright, :authors, :urls, :image, :generator, :ttl, :skip_hours, :skip_days]
# Elements that contain both HTML and escaped HTML.
BLENDED_ELEMENTS = [:items]
ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
attr_accessor(*ELEMENTS)
attr_accessor(:parser)
alias :entries :items
def initialize(wrapper)
# set up associations (i.e. arrays where needed)
@urls = []
@authors = []
@skip_hours = []
@skip_days = []
@items = []
@parser = wrapper.parser.to_s
@last_updated = nil
end
undef last_updated
def last_updated
@last_updated = reparse(@last_updated)
end
def channel() self end
end
end