uformats 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ require 'uformats/basic'
2
+
3
+ module Microformats
4
+ module HCalendar
5
+ class Event < BasicNestedFormat
6
+
7
+ identifier :vevent
8
+
9
+ structure(
10
+ :version => :string,
11
+ :url => :url,
12
+ :summary => :string,
13
+ :dtstart => :datetime,
14
+ :dtend => :datetime,
15
+ :dtstamp => :datetime,
16
+ :location => :string,
17
+ :description => :string,
18
+ :uid => :string
19
+ )
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,100 @@
1
+ require 'uformats/basic'
2
+
3
+ module Microformats
4
+ class HCard < BasicNestedFormat
5
+
6
+ identifier :vcard
7
+
8
+ structure(
9
+ :fn => :string,
10
+ :n => {
11
+ :family_name => :string,
12
+ :given_name => :string,
13
+ :additional_names => :string,
14
+ :honorific_prefix => [:string],
15
+ :honorific_suffix => [:string],
16
+ },
17
+ :nickname => [:string],
18
+ :sort_string => :string,
19
+ :url => [:url],
20
+ :email => [{
21
+ :type => [:string],
22
+ :value => :email,
23
+ }],
24
+ :tel => [{
25
+ :type => [:string],
26
+ :value => :value,
27
+ }],
28
+ :adr => [{
29
+ :post_office_box => :string,
30
+ :extended_address => :string,
31
+ :street_address => :string,
32
+ :locality => :string,
33
+ :region => :string,
34
+ :postal_code => :string,
35
+ :country_name => :string,
36
+ :type => [:string],
37
+ :value => :string,
38
+ }],
39
+ :label => :string,
40
+ :geo => {
41
+ :latitude => :float,
42
+ :longitude => :float,
43
+ },
44
+ :tz => :string,
45
+ :photo => :url,
46
+ :logo => :url,
47
+ :sound => :string,
48
+ :bday => :datetime,
49
+ :title => :string,
50
+ :role => :string,
51
+ :org => {
52
+ :organization_name => :string,
53
+ :organization_unit => :string,
54
+ },
55
+ :category => [:string],
56
+ :note => [:string],
57
+ :class => :string,
58
+ :key => :string,
59
+ :mailer => :string,
60
+ :uid => :string,
61
+ :rev => :string
62
+ )
63
+
64
+ def post_process!
65
+ n_optimize!
66
+ org_optimize!
67
+ end
68
+
69
+ #
70
+ # Perform implied 'N' optimization
71
+ # http://microformats.org/wiki/hcard#Implied_.22N.22_Optimization
72
+ #
73
+ def n_optimize!
74
+ @tree[:n] ||= MethodHash.new
75
+ if (!@tree[:n][:family_name] && !@tree[:n][:given_name] && @tree[:fn])
76
+ @tree[:n][:family_name], @tree[:n][:given_name] = case @tree[:fn]
77
+ when /^(\w+), (\w+)$/u
78
+ [$1, $2]
79
+ when /^(\w+) (\w)\.?$/u
80
+ [$1, $2]
81
+ when /^(\w+) (\w+)$/u
82
+ [$2, $1]
83
+ end
84
+ end
85
+ end
86
+
87
+ #
88
+ # Perform org optimization (for when organization-name is implicit)
89
+ #
90
+ def org_optimize!
91
+ return if lookup(:org, :organization_name)
92
+ Microformats.each_element_by_class(source, :org) do |element|
93
+ @tree[:org] ||= MethodHash.new
94
+ @tree[:org][:organization_name] = process_as_string(element)
95
+ return
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,182 @@
1
+ require 'uformats/basic'
2
+ require 'uformats/hcard'
3
+
4
+ module Microformats
5
+ class HReview < BasicNestedFormat
6
+
7
+ structure(
8
+ :version => :string,
9
+ :summary => :string,
10
+ :type => :string,
11
+ :item => {
12
+ :fn => :string,
13
+ :url => [:url],
14
+ :photo => [:url],
15
+ :vcard => :hcard,
16
+ :vevent => :event,
17
+ },
18
+ :reviewer => {
19
+ :fn => :string,
20
+ :url => :url,
21
+ :email => :email,
22
+ :vcard => :hcard,
23
+ },
24
+ :dtreviewed => :datetime,
25
+ :rating => [:rating],
26
+ :worst => [:worst],
27
+ :best => [:best],
28
+ :description => :xhtml,
29
+ :permalink => :url
30
+ )
31
+
32
+ VALID_TYPES = %w[product business event person place website url]
33
+
34
+ def post_process!
35
+ fix_anonymous_reviewer!
36
+ fix_ratings!
37
+ end
38
+
39
+ #
40
+ # Returns true if this is a valid hReview
41
+ #
42
+ def valid?
43
+ unless (
44
+ lookup(:item, :fn) || lookup(:item, :url) ||
45
+ lookup(:item, :photo) || lookup(:item, :vcard)
46
+ )
47
+ @validation_message = 'missing item details'
48
+ return false
49
+ end
50
+ if (
51
+ lookup(:type) &&
52
+ !VALID_TYPES.include?(lookup(:type))
53
+ )
54
+ @validation_message = 'invalid review type'
55
+ return false
56
+ end
57
+ if (
58
+ (lookup(:version) || 999).to_f > 0.2 &&
59
+ %w[person business].include?(lookup(:type)) &&
60
+ !lookup(:item, :vcard)
61
+ )
62
+ @validation_message = 'item must be vcard for person or business'
63
+ return false
64
+ end
65
+ return true
66
+ end
67
+ attr_accessor :validation_message
68
+
69
+ def license
70
+ return (licenses || []).first
71
+ end
72
+
73
+ def licenses
74
+ return @licenses ||= Microformats.rel_licenses(@source)
75
+ end
76
+
77
+ def process_as_hcard(element)
78
+ return HCard.new(element)
79
+ end
80
+
81
+ def process_as_event(element)
82
+ return HCalendar::Event.new(element)
83
+ end
84
+
85
+ def process_as_rating(element)
86
+ value = process_as_float(element)
87
+ tags = Microformats.rel_tags_above_element(element)
88
+ if !value && value_element = Microformats.first_element_by_class(element, :value)
89
+ value = process_as_float(value_element)
90
+ tags = Microformats.rel_tags_above_element(value_element)
91
+ end
92
+ return set_score(:score=, value, tags)
93
+ end
94
+
95
+ def process_as_best(element)
96
+ value = process_as_float(element)
97
+ tags = Microformats.rel_tags_above_element(element)
98
+ return set_score(:best=, value, tags)
99
+ end
100
+
101
+ def process_as_worst(element)
102
+ value = process_as_float(element)
103
+ tags = Microformats.rel_tags_above_element(element)
104
+ return set_score(:worst=, value, tags)
105
+ end
106
+
107
+ def set_score(sym, value, tags)
108
+ @ratings ||= {}
109
+ if tags
110
+ tags.each do |tag|
111
+ @ratings[tag] ||= Rating.new
112
+ @ratings[tag].__send__(sym, value)
113
+ end
114
+ else
115
+ @rating ||= Rating.new
116
+ @rating.__send__(sym, value)
117
+ end
118
+ return nil
119
+ end
120
+
121
+ def fix_anonymous_reviewer!
122
+ return if lookup(:reviewer, :fn)
123
+ Microformats.each_element_by_class(@source, :reviewer) do |e|
124
+ if 'anonymous' == process_as_string(e).downcase
125
+ @tree[:reviewer][:fn] = 'anonymous'
126
+ return
127
+ end
128
+ end
129
+ end
130
+
131
+ #
132
+ # Assign default values for ratings, and insert them into
133
+ # the tree
134
+ #
135
+ def fix_ratings!
136
+ if (@rating)
137
+ @rating.best ||= Rating::BEST
138
+ @rating.worst ||= Rating::WORST
139
+ default_best = @rating.best
140
+ default_worst = @rating.worst
141
+ end
142
+ default_best ||= Rating::BEST
143
+ default_worst ||= Rating::WORST
144
+ @ratings ||= {}
145
+ @ratings.each do |tag, rating|
146
+ rating.best ||= default_best
147
+ rating.worst ||= default_worst
148
+ end
149
+ @tree[:rating] = @rating
150
+ @tree[:ratings] = @ratings
151
+ end
152
+
153
+ class Rating
154
+ WORST = 1
155
+ BEST = 5
156
+
157
+ def initialize(score=nil)
158
+ @score = score
159
+ end
160
+
161
+ attr_reader :best, :worst, :score
162
+
163
+ def best=(b)
164
+ @best = b.to_i
165
+ end
166
+
167
+ def worst=(w)
168
+ @worst = w.to_i
169
+ end
170
+
171
+ def score=(s)
172
+ @score = s.to_f
173
+ end
174
+
175
+ def to_f
176
+ return (@score - (@worst || WORST)).to_f / ((@best || BEST) - (@worst || WORST))
177
+ end
178
+ end # Rating
179
+
180
+ end # HReview
181
+
182
+ end
@@ -0,0 +1,32 @@
1
+ module Microformats
2
+ class Pluralizer
3
+ @@plurals = {}
4
+
5
+ PLURALS = [
6
+ [/([^aeo])y$/, '\1ies'],
7
+ [/(s+|ch|x)$/, '\1es'],
8
+ ]
9
+
10
+ def self.pluralize(sym)
11
+ return @@plurals[sym] if @@plurals[sym]
12
+ string = sym.to_s
13
+ PLURALS.each do |match, replacement|
14
+ if string.match(match)
15
+ plural = string.sub(match, replacement).to_sym
16
+ @@plurals[sym] = plural
17
+ return plural
18
+ end
19
+ end
20
+ plural = (string + 's').to_sym
21
+ @@plurals[sym] = plural
22
+ return plural
23
+ end
24
+
25
+ def self.singularized_lookup(hash, sym)
26
+ return hash[sym] if hash[sym]
27
+ plural = hash[Pluralizer.pluralize(sym)]
28
+ return plural[0] if plural.respond_to?(:[])
29
+ return nil
30
+ end
31
+ end # Pluralizer
32
+ end
@@ -0,0 +1,4 @@
1
+ $: << File.dirname(__FILE__)
2
+ Dir[File.dirname(__FILE__)+'/*_test.rb'].each do |test|
3
+ require test
4
+ end
@@ -0,0 +1,441 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+ require 'uformats/hcard'
3
+
4
+ class TestFormat < Microformats::BasicNestedFormat
5
+ structure(
6
+ :tv => {
7
+ :type => [:string],
8
+ :value => :value,
9
+ },
10
+ :te => {
11
+ :type => [:string],
12
+ :value => :email,
13
+ },
14
+ :string => :string,
15
+ :nested => {
16
+ :inner => :string,
17
+ },
18
+ :integer => :integer,
19
+ :float => :float,
20
+ :datetime => :datetime,
21
+ :url => :url,
22
+ :email => :email,
23
+ :xhtml => :xhtml
24
+ )
25
+ end
26
+
27
+ class TestFormatWithPostProcess < Microformats::BasicNestedFormat
28
+
29
+ structure(:string => :string)
30
+
31
+ def post_process!
32
+ @post = true
33
+ end
34
+ attr_reader :post
35
+ end
36
+
37
+ class BasicNestedFormatTest < Test::Unit::TestCase
38
+
39
+ def test_should_accept_rexml_document_as_source
40
+ assert_nothing_raised{
41
+ t = TestFormat.new(REXML::Document.new(%{
42
+ <x class="testformat">
43
+ </x>
44
+ }))
45
+ }
46
+ end
47
+
48
+ def test_should_accept_text_as_source
49
+ assert_nothing_raised{
50
+ t = TestFormat.new(%{
51
+ <x class="testformat">
52
+ </x>
53
+ })
54
+ }
55
+ end
56
+
57
+ def test_should_call_post_process
58
+ t = TestFormatWithPostProcess.new('<x></x>')
59
+ assert(t.post)
60
+ end
61
+
62
+ def test_should_not_cause_error_if_post_process_not_defined
63
+ assert_nothing_raised{ TestFormat.new('<x></x>') }
64
+ end
65
+
66
+ def test_should_expand_include_patterns_before_processing
67
+ before = %{
68
+ <div>
69
+ <span class="testformat">
70
+ <span class="string" id="master">Foo</span>
71
+ </span>
72
+ <span class="testformat">
73
+ <object data="#master" class="include" type="text/html"></object>
74
+ </span>
75
+ </div>
76
+ }
77
+ tfs = []
78
+ TestFormat.each(before) do |tf|
79
+ tfs << tf
80
+ end
81
+ assert_equal('Foo', tfs[0].string)
82
+ assert_equal('Foo', tfs[1].string)
83
+ end
84
+
85
+ def test_should_expand_relative_urls
86
+ t = TestFormat.new(%{
87
+ <x class="testformat">
88
+ <a class="url" href="/link">link</a>
89
+ </x>
90
+ }, 'http://www.example.com/')
91
+ assert_equal('http://www.example.com/link', t.url)
92
+ end
93
+
94
+ def test_should_return_nested_item_or_nil_via_lookup_method
95
+ t = TestFormat.new(%{
96
+ <x class="testformat">
97
+ <x class="tv">
98
+ <x class="type">a</x>
99
+ <x class="type">b</x>
100
+ <x class="value">c</x>
101
+ </x>
102
+ </x>
103
+ }, 'http://www.example.com/')
104
+ assert_equal('a', t.lookup(:tv, :type))
105
+ assert_equal('a', t.lookup(:tv, :types, 0))
106
+ assert_equal('b', t.lookup(:tv, :types, 1))
107
+ assert_equal('c', t.lookup(:tv, :value))
108
+ assert_nil(t.lookup(:foo, :bar))
109
+ end
110
+
111
+ def test_should_return_email_when_href
112
+ t = TestFormat.new(%{
113
+ <x class="testformat">
114
+ <a class="email" href="mailto:bob@example.com">email</a>
115
+ </x>
116
+ })
117
+ assert_equal('bob@example.com', t.email)
118
+ end
119
+
120
+ def test_should_return_email_when_text
121
+ t = TestFormat.new(%{
122
+ <x class="testformat">
123
+ <a class="email">bob@example.com</a>
124
+ </x>
125
+ })
126
+ assert_equal('bob@example.com', t.email)
127
+ end
128
+
129
+
130
+ def test_should_interpret_nested_values_when_xml_is_nested
131
+ t = TestFormat.new(%{
132
+ <x class="testformat">
133
+ <a><b class="nested"><c class="inner">foo</c>bar</b></a>
134
+ </x>
135
+ })
136
+ assert_equal('foo', t.nested.inner)
137
+ end
138
+
139
+ def test_should_interpret_nested_values_when_xml_is_not_nested
140
+ t = TestFormat.new(%{
141
+ <x class="testformat">
142
+ <a><b class="nested inner">foo</b>bar</a>
143
+ </x>
144
+ })
145
+ assert_equal('foo', t.nested.inner)
146
+ end
147
+
148
+ def test_should_return_nil_for_missing_href_in_url
149
+ t = TestFormat.new(%{
150
+ <x class="testformat">
151
+ <a class="url">
152
+ quux
153
+ </a>
154
+ </x>
155
+ })
156
+ assert_nil(t.url)
157
+ end
158
+
159
+ def test_should_process_type_and_value_when_value_is_explicit
160
+ t = TestFormat.new(%{
161
+ <x class="testformat">
162
+ <a class="tv">
163
+ <b class="type">foo</b>
164
+ <b class="type">bar</b>
165
+ <b class="value">baz</b>
166
+ quux
167
+ </a>
168
+ </x>
169
+ })
170
+ assert_equal('foo', t.tv.type)
171
+ assert_equal(['foo', 'bar'], t.tv.types)
172
+ assert_equal('baz', t.tv.value)
173
+ end
174
+
175
+ def test_should_process_type_and_value_when_value_is_implicit
176
+ t = TestFormat.new(%{
177
+ <x class="testformat">
178
+ <a class="tv">
179
+ <b class="type">foo</b>
180
+ <b class="type">bar</b>
181
+ baz
182
+ </a>
183
+ </x>
184
+ })
185
+ assert_equal('foo', t.tv.type)
186
+ assert_equal(['foo', 'bar'], t.tv.types)
187
+ assert_equal('baz', t.tv.value)
188
+ end
189
+
190
+ def test_should_process_type_and_email_when_value_is_explicit
191
+ t = TestFormat.new(%{
192
+ <x class="testformat">
193
+ <a class="te">
194
+ <b class="type">foo</b>
195
+ <b class="type">bar</b>
196
+ <b class="value" href="mailto:user@example.com">user@example.com</b>
197
+ quux
198
+ </a>
199
+ </x>
200
+ })
201
+ assert_equal('foo', t.te.type)
202
+ assert_equal(['foo', 'bar'], t.te.types)
203
+ assert_equal('user@example.com', t.te.value)
204
+ end
205
+
206
+ def test_should_process_type_and_email_when_value_is_implicit_href
207
+ t = TestFormat.new(%{
208
+ <x class="testformat">
209
+ <a class="te" href="mailto:user@example.com">
210
+ user@example.com
211
+ <b class="type">foo</b>
212
+ <b class="type">bar</b>
213
+ </a>
214
+ </x>
215
+ })
216
+ assert_equal('foo', t.te.type)
217
+ assert_equal(['foo', 'bar'], t.te.types)
218
+ assert_equal('user@example.com', t.te.value)
219
+ end
220
+
221
+ def test_should_process_type_and_email_when_value_is_implicit_text
222
+ t = TestFormat.new(%{
223
+ <x class="testformat">
224
+ <a class="te">
225
+ user@example.com
226
+ </a>
227
+ </x>
228
+ })
229
+ assert_equal('user@example.com', t.te.value)
230
+ end
231
+
232
+ def test_should_reassemble_marked_up_strings
233
+ t = TestFormat.new(%{
234
+ <x class="testformat">
235
+ <a class="string">
236
+ <h1>About</h1>
237
+ <p>This is <a href="/">some text</a>.</p>
238
+ <h2>Final</h2>
239
+ <p>This is the last paragraph.</p>
240
+ </a>
241
+ </x>
242
+ })
243
+ assert_equal('About This is some text. Final This is the last paragraph.', t.string)
244
+ end
245
+
246
+ def test_should_regurgitate_xhtml_verbatim
247
+ xhtml = %{
248
+ <h1>About</h1>
249
+ <p>This is <a href='/'>some text</a>.</p>
250
+ <h2>Final</h2>
251
+ <p>This is the last paragraph.</p>
252
+ }
253
+ t = TestFormat.new(%{
254
+ <x class="testformat">
255
+ <a class="xhtml">
256
+ #{xhtml}
257
+ </a>
258
+ </x>
259
+ })
260
+ assert_equal(xhtml.strip.gsub(/\s+/, ' '), t.xhtml.strip.gsub(/\s+/, ' '))
261
+ end
262
+
263
+ def test_should_use_title_attribute_when_string_is_abbr
264
+ t = TestFormat.new(%{
265
+ <x class="testformat">
266
+ <abbr class="string" title="foo">bar</abbr>
267
+ </x>
268
+ })
269
+ assert_equal('foo', t.string)
270
+ end
271
+
272
+ def test_should_use_alt_attribute_when_string_is_img
273
+ t = TestFormat.new(%{
274
+ <x class="testformat">
275
+ <img class="string" alt="foo" />
276
+ </x>
277
+ })
278
+ assert_equal('foo', t.string)
279
+ end
280
+
281
+ def test_should_use_href_for_url_when_available
282
+ t = TestFormat.new(%{
283
+ <x class="testformat">
284
+ <a class="url" href="http://example.com/foo">bar</a>
285
+ </x>
286
+ })
287
+ assert_equal('http://example.com/foo', t.url)
288
+ end
289
+
290
+ def test_should_use_src_for_url_when_available
291
+ t = TestFormat.new(%{
292
+ <x class="testformat">
293
+ <img class="url" src="http://example.com/foo" />
294
+ </x>
295
+ })
296
+ assert_equal('http://example.com/foo', t.url)
297
+ end
298
+
299
+ def test_should_find_all_matching_microformats_via_each_with_text
300
+ src = %{
301
+ <z>
302
+ <x class="testformat">
303
+ <a class="integer">1</a>
304
+ </x>
305
+ <x class="testformat">
306
+ <a class="integer">2</a>
307
+ </x>
308
+ </z>
309
+ }
310
+ ary = []
311
+ TestFormat.each(src) do |tf|
312
+ ary << tf
313
+ end
314
+ assert_equal(1, ary[0].integer)
315
+ assert_equal(2, ary[1].integer)
316
+ end
317
+
318
+ def test_should_find_all_matching_microformats_via_each_with_rexml
319
+ src = REXML::Document.new(%{
320
+ <z>
321
+ <x class="testformat">
322
+ <a class="integer">1</a>
323
+ </x>
324
+ <x class="testformat">
325
+ <a class="integer">2</a>
326
+ </x>
327
+ </z>
328
+ })
329
+ ary = []
330
+ TestFormat.each(src) do |tf|
331
+ ary << tf
332
+ end
333
+ assert_equal(1, ary[0].integer)
334
+ assert_equal(2, ary[1].integer)
335
+ end
336
+
337
+
338
+ def test_should_find_first_matching_microformats_via_first
339
+ src = %{
340
+ <z>
341
+ <x class="testformat">
342
+ <a class="integer">1</a>
343
+ </x>
344
+ <x class="testformat">
345
+ <a class="integer">2</a>
346
+ </x>
347
+ </z>
348
+ }
349
+ tf = TestFormat.first(src)
350
+ assert_equal(1, tf.integer)
351
+ end
352
+
353
+
354
+ def test_should_parse_integers
355
+ t = TestFormat.new(%{
356
+ <x class="testformat">
357
+ <a class="integer">09</a>
358
+ </x>
359
+ })
360
+ assert_equal(9, t.integer)
361
+ end
362
+
363
+ def test_should_parse_floats
364
+ t = TestFormat.new(%{
365
+ <x class="testformat">
366
+ <a class="float">3.14159</a>
367
+ </x>
368
+ })
369
+ assert_in_delta(3.14159, t.float, 0.01)
370
+ end
371
+
372
+ def test_should_parse_zulu_dates
373
+ t = TestFormat.new(%{
374
+ <x class="testformat">
375
+ <a class="datetime">20050102T030405Z</a>
376
+ </x>
377
+ })
378
+ assert_equal(2005, t.datetime.utc.year)
379
+ assert_equal(1, t.datetime.utc.month)
380
+ assert_equal(2, t.datetime.utc.day)
381
+ assert_equal(3, t.datetime.utc.hour)
382
+ assert_equal(4, t.datetime.utc.min)
383
+ assert_equal(5, t.datetime.utc.sec)
384
+ end
385
+
386
+ def test_should_parse_partial_dates
387
+ t = TestFormat.new(%{
388
+ <x class="testformat">
389
+ <a class="datetime">200501</a>
390
+ </x>
391
+ })
392
+ assert_equal(2005, t.datetime.utc.year)
393
+ assert_equal(1, t.datetime.utc.month)
394
+ end
395
+
396
+ def test_should_return_nil_for_invalid_date
397
+ t = TestFormat.new(%{
398
+ <x class="testformat">
399
+ <a class="datetime">Hello!</a>
400
+ </x>
401
+ })
402
+ assert_nil(t.datetime)
403
+ t = TestFormat.new(%{
404
+ <x class="testformat">
405
+ <a class="datetime">9999-88-88</a>
406
+ </x>
407
+ })
408
+ assert_nil(t.datetime)
409
+ end
410
+
411
+ def test_should_parse_time_zones_with_multiple_pluses
412
+ t = TestFormat.new(%{
413
+ <x class="testformat">
414
+ <a class="datetime">20010101T120000++0200</a>
415
+ </x>
416
+ })
417
+ assert_equal(10, t.datetime.utc.hour)
418
+ end
419
+
420
+ def test_should_parse_time_zones_with_multiple_minuses
421
+ t = TestFormat.new(%{
422
+ <x class="testformat">
423
+ <a class="datetime">20010101T120000--0200</a>
424
+ </x>
425
+ })
426
+ assert_equal(14, t.datetime.utc.hour)
427
+ end
428
+
429
+
430
+ def test_should_handle_email_when_encapsulated_by_unknown_element
431
+ t = TestFormat.new(%{
432
+ <x class="testformat">
433
+ <div class="te">
434
+ <a class="internet" href="mailto:info@example.com">info@example.com</a>
435
+ </div>
436
+ </x>
437
+ })
438
+ assert_equal('info@example.com', t.te.value)
439
+ end
440
+
441
+ end