brauser 1.0.7 → 2.0.0

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.
Files changed (40) hide show
  1. data/.travis.yml +1 -1
  2. data/Gemfile +1 -1
  3. data/README.md +6 -3
  4. data/Rakefile +1 -1
  5. data/brauser.gemspec +10 -8
  6. data/doc/Brauser.html +4 -4
  7. data/doc/Brauser/Browser.html +1722 -3546
  8. data/doc/Brauser/BrowserMethods.html +125 -0
  9. data/doc/Brauser/BrowserMethods/Attributes.html +484 -0
  10. data/doc/Brauser/BrowserMethods/General.html +134 -0
  11. data/doc/Brauser/BrowserMethods/General/ClassMethods.html +529 -0
  12. data/doc/Brauser/BrowserMethods/Parsing.html +350 -0
  13. data/doc/Brauser/BrowserMethods/PartialQuerying.html +629 -0
  14. data/doc/Brauser/BrowserMethods/Querying.html +583 -0
  15. data/doc/Brauser/BrowserMethods/Register.html +134 -0
  16. data/doc/Brauser/BrowserMethods/Register/ClassMethods.html +1081 -0
  17. data/doc/Brauser/Hooks.html +3 -3
  18. data/doc/Brauser/Hooks/RubyOnRails.html +3 -3
  19. data/doc/Brauser/Query.html +470 -52
  20. data/doc/Brauser/Version.html +5 -5
  21. data/doc/_index.html +102 -4
  22. data/doc/class_list.html +1 -1
  23. data/doc/file.README.html +11 -8
  24. data/doc/frames.html +1 -1
  25. data/doc/index.html +11 -8
  26. data/doc/method_list.html +61 -61
  27. data/doc/top-level-namespace.html +3 -3
  28. data/lib/brauser.rb +1 -1
  29. data/lib/brauser/browser.rb +725 -674
  30. data/lib/brauser/hooks.rb +1 -1
  31. data/lib/brauser/query.rb +6 -4
  32. data/lib/brauser/version.rb +3 -3
  33. data/spec/brauser/browser_spec.rb +15 -38
  34. data/spec/brauser/hooks_spec.rb +1 -1
  35. data/spec/brauser/query_spec.rb +1 -1
  36. data/spec/brauser_spec.rb +1 -1
  37. data/spec/coverage_helper.rb +1 -1
  38. data/spec/frameworks_helper.rb +1 -1
  39. data/spec/spec_helper.rb +1 -1
  40. metadata +25 -22
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  Top Level Namespace
8
8
 
9
- &mdash; Documentation by YARD 0.8.2.1
9
+ &mdash; Documentation by YARD 0.8.3
10
10
 
11
11
  </title>
12
12
 
@@ -103,9 +103,9 @@
103
103
  </div>
104
104
 
105
105
  <div id="footer">
106
- Generated on Mon Oct 22 08:39:39 2012 by
106
+ Generated on Sat Feb 2 17:47:18 2013 by
107
107
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
108
- 0.8.2.1 (ruby-1.9.2).
108
+ 0.8.3 (ruby-1.9.3).
109
109
  </div>
110
110
 
111
111
  </body>
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
  #
3
- # This file is part of the brauser gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
3
+ # This file is part of the brauser gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
4
  # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
5
  #
6
6
 
@@ -1,678 +1,755 @@
1
1
  # encoding: utf-8
2
2
  #
3
- # This file is part of the brauser gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
3
+ # This file is part of the brauser gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
4
  # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
5
  #
6
6
 
7
7
  # A framework agnostic browser detection and querying helper.
8
8
  module Brauser
9
- # This class represents a detection of the current user browser.
10
- class Browser
11
- # The raw User-Agent HTTP header.
12
- attr_accessor :agent
13
-
14
- # The raw Accept-Language HTTP header.
15
- attr_accessor :accept_language
16
-
17
- # The accepted languages.
18
- attr_accessor :languages
19
-
20
- # The current browser name.
21
- attr_accessor :name
22
-
23
- # The current browser version.
24
- attr_accessor :version
9
+ # Methods of the {Browser Browser} class.
10
+ module BrowserMethods
11
+ # Methods for register recognized browsers, versions and platforms.
12
+ module Register
13
+ extend ActiveSupport::Concern
14
+
15
+ # Class methods.
16
+ module ClassMethods
17
+ # Registers the default list of browsers that can be recognized.
18
+ #
19
+ # @return [Boolean] `true` if at least one browser has been added, `false` otherwise.
20
+ def register_default_browsers
21
+ @browsers = nil
22
+
23
+ register_mobile_browsers
24
+ register_desktop_browsers
25
+
26
+ @browsers.present? ? true : false
27
+ end
25
28
 
26
- # The current browser platform.
27
- attr_accessor :platform
29
+ # Registers the default list of platforms that can be recognized.
30
+ #
31
+ # @return [Boolean] `true` if at least one platform has been added, `false` otherwise.
32
+ def register_default_platforms
33
+ @platforms = nil
34
+
35
+ self.register_platform([
36
+ [:symbian, /s60|symb/i, "Symbian"],
37
+ [:windows_phone, /windows phone/i, "Microsoft Windows Phone"],
38
+ [:kindle, Proc.new { |name, agent| name == :kindle }, "Nokia Symbian"],
39
+ [:ios, Proc.new { |name, agent| [:iphone, :ipad, :ipod].include?(name) || agent =~ /ipad|iphone|ipod/i }, "Apple iOS"],
40
+ [:android, /android/i, "Android"],
41
+ [:blackberry, /blackberry/i, "RIM BlackBerry"],
42
+ [:psp, /psp/i, "Sony Playstation Portable"],
43
+ [:ps3, /playstation 3/i, "Sony Playstation 3"],
44
+ [:wii, /wii/i, "Nintendo Wii"],
45
+
46
+ [:linux, /linux/i, "Linux"],
47
+ [:osx, /mac|macintosh|mac os x/i, "Apple MacOS X"],
48
+ [:windows, /windows/i, "Microsoft Windows"]
49
+ ])
50
+
51
+ @platforms.present? ? true : false
52
+ end
28
53
 
29
- # Aliases
30
- alias :ua :agent
31
- alias :ua= :agent=
54
+ # Registers the default list of languages that can be recognized.
55
+ #
56
+ # @return [Boolean] `true` if at least one language has been added, `false` otherwise.
57
+ def register_default_languages
58
+ @languages = nil
59
+
60
+ self.register_language({
61
+ "af" => "Afrikaans",
62
+ "sq" => "Albanian",
63
+ "eu" => "Basque",
64
+ "bg" => "Bulgarian",
65
+ "be" => "Byelorussian",
66
+ "ca" => "Catalan",
67
+ "zh" => "Chinese",
68
+ "zh-cn" => "Chinese/China",
69
+ "zh-tw" => "Chinese/Taiwan",
70
+ "zh-hk" => "Chinese/Hong Kong",
71
+ "zh-sg" => "Chinese/singapore",
72
+ "hr" => "Croatian",
73
+ "cs" => "Czech",
74
+ "da" => "Danish",
75
+ "nl" => "Dutch",
76
+ "nl-nl" => "Dutch/Netherlands",
77
+ "nl-be" => "Dutch/Belgium",
78
+ "en" => "English",
79
+ "en-gb" => "English/United Kingdom",
80
+ "en-us" => "English/United States",
81
+ "en-au" => "English/Australian",
82
+ "en-ca" => "English/Canada",
83
+ "en-nz" => "English/New Zealand",
84
+ "en-ie" => "English/Ireland",
85
+ "en-za" => "English/South Africa",
86
+ "en-jm" => "English/Jamaica",
87
+ "en-bz" => "English/Belize",
88
+ "en-tt" => "English/Trinidad",
89
+ "et" => "Estonian",
90
+ "fo" => "Faeroese",
91
+ "fa" => "Farsi",
92
+ "fi" => "Finnish",
93
+ "fr" => "French",
94
+ "fr-be" => "French/Belgium",
95
+ "fr-fr" => "French/France",
96
+ "fr-ch" => "French/Switzerland",
97
+ "fr-ca" => "French/Canada",
98
+ "fr-lu" => "French/Luxembourg",
99
+ "gd" => "Gaelic",
100
+ "gl" => "Galician",
101
+ "de" => "German",
102
+ "de-at" => "German/Austria",
103
+ "de-de" => "German/Germany",
104
+ "de-ch" => "German/Switzerland",
105
+ "de-lu" => "German/Luxembourg",
106
+ "de-li" => "German/Liechtenstein",
107
+ "el" => "Greek",
108
+ "he" => "Hebrew",
109
+ "he-il" => "Hebrew/Israel",
110
+ "hi" => "Hindi",
111
+ "hu" => "Hungarian",
112
+ "ie-ee" => "Internet Explorer/Easter Egg",
113
+ "is" => "Icelandic",
114
+ "id" => "Indonesian",
115
+ "in" => "Indonesian",
116
+ "ga" => "Irish",
117
+ "it" => "Italian",
118
+ "it-ch" => "Italian/ Switzerland",
119
+ "ja" => "Japanese",
120
+ "km" => "Khmer",
121
+ "km-kh" => "Khmer/Cambodia",
122
+ "ko" => "Korean",
123
+ "lv" => "Latvian",
124
+ "lt" => "Lithuanian",
125
+ "mk" => "Macedonian",
126
+ "ms" => "Malaysian",
127
+ "mt" => "Maltese",
128
+ "no" => "Norwegian",
129
+ "pl" => "Polish",
130
+ "pt" => "Portuguese",
131
+ "pt-br" => "Portuguese/Brazil",
132
+ "rm" => "Rhaeto-Romanic",
133
+ "ro" => "Romanian",
134
+ "ro-mo" => "Romanian/Moldavia",
135
+ "ru" => "Russian",
136
+ "ru-mo" => "Russian /Moldavia",
137
+ "gd" => "Scots Gaelic",
138
+ "sr" => "Serbian",
139
+ "sk" => "Slovack",
140
+ "sl" => "Slovenian",
141
+ "sb" => "Sorbian",
142
+ "es" => "Spanish",
143
+ "es-do" => "Spanish",
144
+ "es-ar" => "Spanish/Argentina",
145
+ "es-co" => "Spanish/Colombia",
146
+ "es-mx" => "Spanish/Mexico",
147
+ "es-es" => "Spanish/Spain",
148
+ "es-gt" => "Spanish/Guatemala",
149
+ "es-cr" => "Spanish/Costa Rica",
150
+ "es-pa" => "Spanish/Panama",
151
+ "es-ve" => "Spanish/Venezuela",
152
+ "es-pe" => "Spanish/Peru",
153
+ "es-ec" => "Spanish/Ecuador",
154
+ "es-cl" => "Spanish/Chile",
155
+ "es-uy" => "Spanish/Uruguay",
156
+ "es-py" => "Spanish/Paraguay",
157
+ "es-bo" => "Spanish/Bolivia",
158
+ "es-sv" => "Spanish/El salvador",
159
+ "es-hn" => "Spanish/Honduras",
160
+ "es-ni" => "Spanish/Nicaragua",
161
+ "es-pr" => "Spanish/Puerto Rico",
162
+ "sx" => "Sutu",
163
+ "sv" => "Swedish",
164
+ "sv-se" => "Swedish/Sweden",
165
+ "sv-fi" => "Swedish/Finland",
166
+ "ts" => "Thai",
167
+ "tn" => "Tswana",
168
+ "tr" => "Turkish",
169
+ "uk" => "Ukrainian",
170
+ "ur" => "Urdu",
171
+ "vi" => "Vietnamese",
172
+ "xh" => "Xshosa",
173
+ "ji" => "Yiddish",
174
+ "zu" => "Zulu"
175
+ })
176
+
177
+ @languages.present? ? true : false
178
+ end
32
179
 
33
- # Returns the list of browser that can be recognized.
34
- #
35
- # The keys are the browser name, the values are arrays of the name matcher, the version match and the label.
36
- #
37
- # @return [Hash] The list of browser that can be recognized.
38
- def self.browsers
39
- rv = ActiveSupport::OrderedHash.new
180
+ # Registers a new browser that can be recognized.
181
+ #
182
+ # @param name [Symbol|Array] The browser name or a list of browser (a list of array with `[name, name_match, version_match, label]` entries).
183
+ # @param name_match [String|Regexp|Block] The matcher for the name. If a block, it will be yield with the user agent and must return `true` if the name was recognized.
184
+ # @param version_match [String|Regexp|Block] The match for the version. If a block, it will be yield with the browser name and the user agent and must return the browser version.
185
+ # @param label [String] A human readable name of the browser.
186
+ # @return [Boolean] `true` if at least one browser has been added, `false` otherwise.
187
+ def register_browser(name, name_match = nil, version_match = nil, label = nil)
188
+ @browsers ||= []
189
+ register_entries(@browsers, (name.is_a?(Array) ? name : [[name.ensure_string, name_match, version_match, label]]))
190
+ end
40
191
 
41
- @browsers.each do |browser|
42
- rv[browser[0]] = browser[1, browser.length]
43
- end
192
+ # Registers a new platform that can be recognized.
193
+ #
194
+ # @param name [Symbol|Array] The platform name or a list of platforms (a list of array with `[name, matcher, label]` entries).
195
+ # @param matcher [StringRegexp|Block] The matcher for the platform. If a block, it will be yielded with the browser name and the user agent and must return `true` if the platform was recognized.
196
+ # @param label [String] A human readable name of the platform.
197
+ # @return [Boolean] `true` if at least one platform has been added, `false` otherwise.
198
+ def register_platform(name, matcher = nil, label = nil)
199
+ @platforms ||= []
200
+ register_entries(@platforms, (name.is_a?(Array) ? name : [[name.ensure_string, matcher, label]]))
201
+ end
44
202
 
45
- rv
46
- end
203
+ # Registers a new language that can be recognized.
204
+ #
205
+ # @param code [String|Hash] The language code or an hash with codes as keys and label as values.
206
+ # @param label [String] The language name. Ignored if code is an Hash.
207
+ # @return [Boolean] `true` if at least one language has been added, `false` otherwise.
208
+ def register_language(code, label = nil)
209
+ @languages ||= {}
210
+ rv = false
211
+ code = {code.ensure_string => label.ensure_string} if !code.is_a?(Hash)
212
+
213
+ code.each_pair do |c, l|
214
+ if c.present? && l.present? then
215
+ @languages[c] = l
216
+ rv = true
217
+ end
218
+ end
47
219
 
48
- # Returns the list of platforms that can be recognized.
49
- #
50
- # The keys are the platform name, values are arrays of the matcher and the label.
51
- #
52
- # @return [Hash] The list of platform that can be recognized.
53
- def self.platforms
54
- rv = ActiveSupport::OrderedHash.new
220
+ rv
221
+ end
55
222
 
56
- @platforms.each do |platform|
57
- rv[platform[0]] = platform[1, platform.length]
58
- end
223
+ private
224
+ # Register the most common desktop browsers.
225
+ # @return [Boolean] `true` if at least one browser has been added, `false` otherwise.
226
+ def register_desktop_browsers
227
+ self.register_browser([
228
+ [:chrome, /((chrome)|(chromium))/i, /(.+Chrom[a-z]+\/)([a-z0-9.]+)/i, "Google Chrome"],
229
+ [:netscape, /(netscape|navigator)\//i, /((Netscape|Navigator)\/)([a-z0-9.]+)/i, "Netscape Navigator"],
230
+ [:firefox, /firefox/i, /(.+Firefox\/)([a-z0-9.]+)/i, "Mozilla Firefox"],
231
+ [:safari, Proc.new{ |agent| agent =~ /safari/i && agent !~ /((chrome)|(chromium))/i }, /(.+Version\/)([a-z0-9.]+)/i, "Apple Safari"],
232
+
233
+ [:msie_compatibility, /trident/i, Proc.new { |name, agent|
234
+ version = /(.+Trident\/)([a-z0-9.]+)/i.match(agent)
235
+
236
+ if version.is_a?(::MatchData) then
237
+ v = version.to_a.last.split(".")
238
+ v[0] = v[0].to_integer + 4
239
+ version = v.join(".")
240
+ end
59
241
 
60
- rv
61
- end
242
+ version
243
+ }, "Microsoft Internet Explorer (Compatibility View)"],
244
+ [:msie, Proc.new{ |agent| agent =~ /msie/i && agent !~ /opera/i }, /(.+MSIE )([a-z0-9.]+)/i, "Microsoft Internet Explorer"],
62
245
 
63
- # Returns the list of languages that can be recognized.
64
- #
65
- # The keys are the languages code, the values the labels.
66
- #
67
- # @return [Hash] The list of languages that can be recognized.
68
- def self.languages
69
- @languages
70
- end
246
+ [:quicktime, /quicktime/i, /(.+((QuickTime\/)|(qtver=)))([a-z0-9.]+)/i, "Apple QuickTime"],
71
247
 
72
- # Registers the default list of browsers that can be recognized.
73
- #
74
- # @return [Boolean] `true` if at least one browser has been added, `false` otherwise.
75
- def self.register_default_browsers
76
- @browsers = nil
77
-
78
- self.register_browser([
79
- [:coremedia, /coremedia/i, /.+CoreMedia v([a-z0-9.]+)/i, "Apple CoreMedia"],
80
-
81
- [:opera_mobile, /opera mobi/i, /.+Opera Mobi.+((.+Opera )|(Version\/))([a-z0-9.]+)/i, "Opera Mobile"],
82
- [:opera, /opera/i, Proc.new{ |name, agent|
83
- regexp = (agent !~ /wii/i) ? /((.+Opera )|(Version\/))([a-z0-9.]+)/i : /(.+Nintendo Wii; U; ; )([a-z0-9.]+)/i
84
-
85
- version = regexp.match(agent)
86
- version = version.to_a.last if version.is_a?(MatchData)
87
- version
88
- }, "Opera"],
89
-
90
- [:android, /android/i, /(.+Android )([a-z0-9.]+)/i, "Android"],
91
- [:blackberry, /blackberry/i, /(.+Version\/)([a-z0-9.]+)/i, "RIM BlackBerry"],
92
- [:kindle, /(kindle)/i, /(.+(Kindle|Silk)\/)([a-z0-9.]+)/i, "Amazon Kindle"],
93
- [:psp, /psp/i, /(.+PlayStation Portable\); )([a-z0-9.]+)/i, "Sony Playstation Portable"],
94
- [:ps3, /playstation 3/i, /(.+PLAYSTATION 3; )([a-z0-9.]+)/i, "Sony Playstation 3"],
95
- [:windows_phone, /windows phone/i, /(.+IEMobile\/)([a-z0-9.]+)/i, "Microsoft Windows Phone"],
96
- [:wii, /nintendo wii/, /(.+Nintendo Wii; U; ; )([a-z0-9.]+)/i, "Nintendo Wii"],
97
-
98
- [:ipod, /ipod/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPod"],
99
- [:iphone, /iphone/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPhone"],
100
- [:ipad, /ipad/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPad"],
101
-
102
- [:mobile, /(mobile|symbian|midp|windows ce)/i, /.+\/([a-z0-9.]+)/i, "Other Mobile Browser"],
103
-
104
- [:chrome, /((chrome)|(chromium))/i, /(.+Chrom[a-z]+\/)([a-z0-9.]+)/i, "Google Chrome"],
105
- [:netscape, /(netscape|navigator)\//i, /((Netscape|Navigator)\/)([a-z0-9.]+)/i, "Netscape Navigator"],
106
- [:firefox, /firefox/i, /(.+Firefox\/)([a-z0-9.]+)/i, "Mozilla Firefox"],
107
- [:safari, Proc.new{ |agent| agent =~ /safari/i && agent !~ /((chrome)|(chromium))/i }, /(.+Version\/)([a-z0-9.]+)/i, "Apple Safari"],
108
-
109
- [:msie_compatibility, /trident/i, Proc.new { |name, agent|
110
- version = /(.+Trident\/)([a-z0-9.]+)/i.match(agent)
111
-
112
- if version.is_a?(::MatchData) then
113
- v = version.to_a.last.split(".")
114
- v[0] = v[0].to_integer + 4
115
- version = v.join(".")
248
+ [:webkit, /webkit/i, /(.+WebKit\/)([a-z0-9.]+)/i, "WebKit Browser"],
249
+ [:gecko, /gecko/i, /(.+rv:|Gecko\/)([a-z0-9.]+)/i, "Gecko Browser"],
250
+ ])
116
251
  end
117
252
 
118
- version
119
- }, "Microsoft Internet Explorer (Compatibility View)"],
120
- [:msie, Proc.new{ |agent| agent =~ /msie/i && agent !~ /opera/i }, /(.+MSIE )([a-z0-9.]+)/i, "Microsoft Internet Explorer"],
121
-
122
- [:quicktime, /quicktime/i, /(.+((QuickTime\/)|(qtver=)))([a-z0-9.]+)/i, "Apple QuickTime"],
253
+ # Register the most common mobile and console browsers.
254
+ # @return [Boolean] `true` if at least one browser has been added, `false` otherwise.
255
+ def register_mobile_browsers
256
+ self.register_browser([
257
+ [:coremedia, /coremedia/i, /.+CoreMedia v([a-z0-9.]+)/i, "Apple CoreMedia"],
258
+
259
+ [:opera_mobile, /opera mobi/i, /.+Opera Mobi.+((.+Opera )|(Version\/))([a-z0-9.]+)/i, "Opera Mobile"],
260
+ [:opera, /opera/i, Proc.new{ |name, agent|
261
+ regexp = (agent !~ /wii/i) ? /((.+Opera )|(Version\/))([a-z0-9.]+)/i : /(.+Nintendo Wii; U; ; )([a-z0-9.]+)/i
262
+
263
+ version = regexp.match(agent)
264
+ version = version.to_a.last if version.is_a?(MatchData)
265
+ version
266
+ }, "Opera"],
267
+
268
+ [:android, /android/i, /(.+Android )([a-z0-9.]+)/i, "Android"],
269
+ [:blackberry, /blackberry/i, /(.+Version\/)([a-z0-9.]+)/i, "RIM BlackBerry"],
270
+ [:kindle, /(kindle)/i, /(.+(Kindle|Silk)\/)([a-z0-9.]+)/i, "Amazon Kindle"],
271
+ [:psp, /psp/i, /(.+PlayStation Portable\); )([a-z0-9.]+)/i, "Sony Playstation Portable"],
272
+ [:ps3, /playstation 3/i, /(.+PLAYSTATION 3; )([a-z0-9.]+)/i, "Sony Playstation 3"],
273
+ [:windows_phone, /windows phone/i, /(.+IEMobile\/)([a-z0-9.]+)/i, "Microsoft Windows Phone"],
274
+ [:wii, /nintendo wii/, /(.+Nintendo Wii; U; ; )([a-z0-9.]+)/i, "Nintendo Wii"],
275
+
276
+ [:ipod, /ipod/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPod"],
277
+ [:iphone, /iphone/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPhone"],
278
+ [:ipad, /ipad/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPad"],
279
+
280
+ [:mobile, /(mobile|symbian|midp|windows ce)/i, /.+\/([a-z0-9.]+)/i, "Other Mobile Browser"],
281
+ ])
282
+ end
123
283
 
124
- [:webkit, /webkit/i, /(.+WebKit\/)([a-z0-9.]+)/i, "WebKit Browser"],
125
- [:gecko, /gecko/i, /(.+rv:|Gecko\/)([a-z0-9.]+)/i, "Gecko Browser"],
126
- ])
284
+ # Registers a new set of entries to a collection.
285
+ #
286
+ # @param collection [Array] The collection which add entries to.
287
+ # @param entries [Array] The entries to add.
288
+ def register_entries(collection, entries)
289
+ rv = false
290
+
291
+ entries.each do |entry|
292
+ entry[0] = entry[0].to_sym
293
+ index = collection.find_index { |item| item[0] == entry[0] }
294
+
295
+ # Replace a previous entry
296
+ if index then
297
+ collection[index] = entry
298
+ else
299
+ collection << entry
300
+ rv = true
301
+ end
302
+ end
127
303
 
128
- @browsers.present? ? true : false
304
+ rv
305
+ end
306
+ end
129
307
  end
130
308
 
131
- # Registers the default list of platforms that can be recognized.
132
- #
133
- # @return [Boolean] `true` if at least one platform has been added, `false` otherwise.
134
- def self.register_default_platforms
135
- @platforms = nil
136
-
137
- self.register_platform([
138
- [:symbian, /s60|symb/i, "Symbian"],
139
- [:windows_phone, /windows phone/i, "Microsoft Windows Phone"],
140
- [:kindle, Proc.new { |name, agent| name == :kindle }, "Nokia Symbian"],
141
- [:ios, Proc.new { |name, agent| [:iphone, :ipad, :ipod].include?(name) || agent =~ /ipad|iphone|ipod/i }, "Apple iOS"],
142
- [:android, /android/i, "Android"],
143
- [:blackberry, /blackberry/i, "RIM BlackBerry"],
144
- [:psp, /psp/i, "Sony Playstation Portable"],
145
- [:ps3, /playstation 3/i, "Sony Playstation 3"],
146
- [:wii, /wii/i, "Nintendo Wii"],
147
-
148
- [:linux, /linux/i, "Linux"],
149
- [:osx, /mac|macintosh|mac os x/i, "Apple MacOS X"],
150
- [:windows, /windows/i, "Microsoft Windows"]
151
- ])
152
-
153
- @platforms.present? ? true : false
154
- end
309
+ # General methods.
310
+ module General
311
+ extend ActiveSupport::Concern
312
+
313
+ # Class methods.
314
+ module ClassMethods
315
+ # Returns the list of browser that can be recognized.
316
+ #
317
+ # The keys are the browser name, the values are arrays of the name matcher, the version match and the label.
318
+ #
319
+ # @return [Hash] The list of browser that can be recognized.
320
+ def browsers
321
+ registered_to_hash(@browsers)
322
+ end
155
323
 
156
- # Registers the default list of languages that can be recognized.
157
- #
158
- # @return [Boolean] `true` if at least one language has been added, `false` otherwise.
159
- def self.register_default_languages
160
- @languages = nil
161
-
162
- self.register_language({
163
- "af" => "Afrikaans",
164
- "sq" => "Albanian",
165
- "eu" => "Basque",
166
- "bg" => "Bulgarian",
167
- "be" => "Byelorussian",
168
- "ca" => "Catalan",
169
- "zh" => "Chinese",
170
- "zh-cn" => "Chinese/China",
171
- "zh-tw" => "Chinese/Taiwan",
172
- "zh-hk" => "Chinese/Hong Kong",
173
- "zh-sg" => "Chinese/singapore",
174
- "hr" => "Croatian",
175
- "cs" => "Czech",
176
- "da" => "Danish",
177
- "nl" => "Dutch",
178
- "nl-nl" => "Dutch/Netherlands",
179
- "nl-be" => "Dutch/Belgium",
180
- "en" => "English",
181
- "en-gb" => "English/United Kingdom",
182
- "en-us" => "English/United States",
183
- "en-au" => "English/Australian",
184
- "en-ca" => "English/Canada",
185
- "en-nz" => "English/New Zealand",
186
- "en-ie" => "English/Ireland",
187
- "en-za" => "English/South Africa",
188
- "en-jm" => "English/Jamaica",
189
- "en-bz" => "English/Belize",
190
- "en-tt" => "English/Trinidad",
191
- "et" => "Estonian",
192
- "fo" => "Faeroese",
193
- "fa" => "Farsi",
194
- "fi" => "Finnish",
195
- "fr" => "French",
196
- "fr-be" => "French/Belgium",
197
- "fr-fr" => "French/France",
198
- "fr-ch" => "French/Switzerland",
199
- "fr-ca" => "French/Canada",
200
- "fr-lu" => "French/Luxembourg",
201
- "gd" => "Gaelic",
202
- "gl" => "Galician",
203
- "de" => "German",
204
- "de-at" => "German/Austria",
205
- "de-de" => "German/Germany",
206
- "de-ch" => "German/Switzerland",
207
- "de-lu" => "German/Luxembourg",
208
- "de-li" => "German/Liechtenstein",
209
- "el" => "Greek",
210
- "he" => "Hebrew",
211
- "he-il" => "Hebrew/Israel",
212
- "hi" => "Hindi",
213
- "hu" => "Hungarian",
214
- "ie-ee" => "Internet Explorer/Easter Egg",
215
- "is" => "Icelandic",
216
- "id" => "Indonesian",
217
- "in" => "Indonesian",
218
- "ga" => "Irish",
219
- "it" => "Italian",
220
- "it-ch" => "Italian/ Switzerland",
221
- "ja" => "Japanese",
222
- "km" => "Khmer",
223
- "km-kh" => "Khmer/Cambodia",
224
- "ko" => "Korean",
225
- "lv" => "Latvian",
226
- "lt" => "Lithuanian",
227
- "mk" => "Macedonian",
228
- "ms" => "Malaysian",
229
- "mt" => "Maltese",
230
- "no" => "Norwegian",
231
- "pl" => "Polish",
232
- "pt" => "Portuguese",
233
- "pt-br" => "Portuguese/Brazil",
234
- "rm" => "Rhaeto-Romanic",
235
- "ro" => "Romanian",
236
- "ro-mo" => "Romanian/Moldavia",
237
- "ru" => "Russian",
238
- "ru-mo" => "Russian /Moldavia",
239
- "gd" => "Scots Gaelic",
240
- "sr" => "Serbian",
241
- "sk" => "Slovack",
242
- "sl" => "Slovenian",
243
- "sb" => "Sorbian",
244
- "es" => "Spanish",
245
- "es-do" => "Spanish",
246
- "es-ar" => "Spanish/Argentina",
247
- "es-co" => "Spanish/Colombia",
248
- "es-mx" => "Spanish/Mexico",
249
- "es-es" => "Spanish/Spain",
250
- "es-gt" => "Spanish/Guatemala",
251
- "es-cr" => "Spanish/Costa Rica",
252
- "es-pa" => "Spanish/Panama",
253
- "es-ve" => "Spanish/Venezuela",
254
- "es-pe" => "Spanish/Peru",
255
- "es-ec" => "Spanish/Ecuador",
256
- "es-cl" => "Spanish/Chile",
257
- "es-uy" => "Spanish/Uruguay",
258
- "es-py" => "Spanish/Paraguay",
259
- "es-bo" => "Spanish/Bolivia",
260
- "es-sv" => "Spanish/El salvador",
261
- "es-hn" => "Spanish/Honduras",
262
- "es-ni" => "Spanish/Nicaragua",
263
- "es-pr" => "Spanish/Puerto Rico",
264
- "sx" => "Sutu",
265
- "sv" => "Swedish",
266
- "sv-se" => "Swedish/Sweden",
267
- "sv-fi" => "Swedish/Finland",
268
- "ts" => "Thai",
269
- "tn" => "Tswana",
270
- "tr" => "Turkish",
271
- "uk" => "Ukrainian",
272
- "ur" => "Urdu",
273
- "vi" => "Vietnamese",
274
- "xh" => "Xshosa",
275
- "ji" => "Yiddish",
276
- "zu" => "Zulu"
277
- })
278
-
279
- @languages.present? ? true : false
280
- end
324
+ # Returns the list of platforms that can be recognized.
325
+ #
326
+ # The keys are the platform name, values are arrays of the matcher and the label.
327
+ #
328
+ # @return [Hash] The list of platform that can be recognized.
329
+ def platforms
330
+ registered_to_hash(@platforms)
331
+ end
281
332
 
282
- # Registers a new browser that can be recognized.
283
- #
284
- # @param name [Symbol|Array] The browser name or a list of browser (a list of array with `[name, name_match, version_match, label]` entries).
285
- # @param name_match [String|Regexp|Block] The matcher for the name. If a block, it will be yield with the user agent and must return `true` if the name was recognized.
286
- # @param version_match [String|Regexp|Block] The match for the version. If a block, it will be yield with the browser name and the user agent and must return the browser version.
287
- # @param label [String] A human readable name of the browser.
288
- # @return [Boolean] `true` if at least one browser has been added, `false` otherwise.
289
- def self.register_browser(name, name_match = nil, version_match = nil, label = nil)
290
- if !@browsers then
291
- @browsers = []
292
- @browsers_indexes = {}
293
- end
333
+ # Returns the list of languages that can be recognized.
334
+ #
335
+ # The keys are the languages code, the values the labels.
336
+ #
337
+ # @return [Hash] The list of languages that can be recognized.
338
+ def languages
339
+ @languages
340
+ end
341
+
342
+ # Compares two versions.
343
+ #
344
+ # @param v1 [String] The first versions to compare.
345
+ # @param operator [Symbol] The operator to use for comparison, can be one of `[:lt, :lte, :eq, :gte, :gt]`.
346
+ # @param v2 [Symbol] The second version to compare.
347
+ # @return [Boolean] true if comparison is valid, `false` otherwise.
348
+ def compare_versions(v1 = "", operator = :eq, v2 = "")
349
+ valid_results = {lt: [-1], lte: [-1, 0], eq: [0], gte: [0, 1], gt: [1]}.fetch(operator, [])
350
+
351
+ if valid_results.present? && v1.ensure_string.present? then
352
+ p1, p2 = find_relevant_tokens(v1.ensure_string.strip, v2.ensure_string.strip)
353
+ p1, p2 = normalize_tokens(p1, p2)
354
+ valid_results.include?(p1 <=> p2)
355
+ else
356
+ false
357
+ end
358
+ end
294
359
 
295
- rv = false
296
- name = [[name.ensure_string.to_sym, name_match, version_match, label]] if !name.is_a?(Array)
360
+ private
361
+ # Find relevant tokens (that is, the first two which are not equals) in a string for comparison.
362
+ #
363
+ # @param v1 [String] The first versions to compare.
364
+ # @param v2 [String] The second version to compare.
365
+ # @return [Array] The tokens to compare.
366
+ def find_relevant_tokens(v1, v2)
367
+ v1 = v1.split(".")
368
+ v2 = v2.split(".")
369
+
370
+ p1 = nil
371
+ p2 = nil
372
+ [v1.length, v2.length].max.times do |i|
373
+ p1 = v1[i]
374
+ p2 = v2[i]
375
+ break if !p1 && !p2 || p1 != p2
376
+ end
297
377
 
298
- name.each do |browser|
299
- browser[0] = browser[0].to_sym
378
+ [p1 || "0", p2 || "0"]
379
+ end
300
380
 
301
- index = @browsers.index do |item|
302
- item[0] == browser[0]
303
- end
381
+ # Normalizes token for comparison.
382
+ #
383
+ # @param p1 [String] The first token to normalize.
384
+ # @param p2 [String] The second token to normalize.
385
+ # @return [Array] The tokens to compare.
386
+ def normalize_tokens(p1, p2)
387
+ if !p1.is_integer? then
388
+ ll = p1.length
389
+ p1 = p2 + p1
390
+ p2 = p2 + ("z" * ll)
391
+ end
304
392
 
305
- # Replace a previous entry
306
- if index then
307
- @browsers[index] = browser
308
- else
309
- @browsers << browser
310
- @browsers_indexes[browser[0]] = @browsers.length - 1
311
- end
393
+ [p1, p2]
394
+ end
312
395
 
313
- rv = true
396
+ # Converts a list of register entries to an ordered hash.
397
+ #
398
+ # @param entries [Array] The array to convert.
399
+ # @return [OrderedHash] An ordered hash.
400
+ def registered_to_hash(entries)
401
+ entries.inject(ActiveSupport::OrderedHash.new) do |rv, entry|
402
+ rv[entry[0]] = entry[1, entry.length]
403
+ rv
404
+ end
405
+ end
314
406
  end
315
-
316
- rv
317
407
  end
318
408
 
319
- # Registers a new platform that can be recognized.
320
- #
321
- # @param name [Symbol|Array] The platform name or a list of platforms (a list of array with `[name, matcher, label]` entries).
322
- # @param matcher [StringRegexp|Block] The matcher for the platform. If a block, it will be yielded with the browser name and the user agent and must return `true` if the platform was recognized.
323
- # @param label [String] A human readable name of the platform.
324
- # @return [Boolean] `true` if at least one platform has been added, `false` otherwise.
325
- def self.register_platform(name, matcher = nil, label = nil)
326
- if !@platforms then
327
- @platforms = []
328
- @platforms_indexes = {}
409
+ # Methods to handle attributes
410
+ module Attributes
411
+ # Gets a human-readable browser name.
412
+ #
413
+ # @return [String] A human-readable browser name.
414
+ def readable_name
415
+ self.parse_agent(@agent) if !@name
416
+ ::Brauser::Browser.browsers.fetch(@name, ["Unknown Browser"]).last.ensure_string
329
417
  end
330
418
 
419
+ # Gets a human-readable platform name.
420
+ #
421
+ # @return [String] A readable platform name.
422
+ def platform_name
423
+ self.parse_agent(@agent) if !@platform
424
+ ::Brauser::Browser.platforms.fetch(@platform, ["Unknown Platform"]).last.ensure_string
425
+ end
331
426
 
332
- rv = false
333
- name = [[name.ensure_string.to_sym, matcher, label]] if !name.is_a?(Array)
334
-
335
- name.each do |platform|
336
- platform[0] = platform[0].to_sym
337
-
338
- index = @platforms.index do |item|
339
- item[0] == platform[0]
427
+ # Returns an array of information about the browser. Information are strings which are suitable to use as CSS classes.
428
+ #
429
+ # For version, it will be included a class for every token of the version. For example, version `7.0.1.2` will return this:
430
+ #
431
+ # ```ruby
432
+ # ["version-7", "version-7_0", "version-7_0_1", "version-7_0_1_2"]
433
+ # ```
434
+ #
435
+ # If you provide a block (with accepts name, version and platform as arguments), it will be used for translating the name.
436
+ #
437
+ # @param join [String|NilClass] If non falsy, the separator to use to join information. If falsy, informations will be returned as array.
438
+ # @param name [Boolean] If non falsy, the string to prepend to the name. If falsy, the name information will not be included.
439
+ # @param version [String|NilClass] If non falsy, the string to prepend to the version. If falsy, the version information will not be included.
440
+ # @param platform [String|NilClass] If non falsy, the string to prepend to the platform. If falsy, the platform information will not be included.
441
+ # @param block [Proc] A block to translate browser name.
442
+ # @return [String|Array] CSS ready information of the current browser.
443
+ def classes(join = " ", name = "", version = "version-", platform = "platform-", &block)
444
+ platform = "platform-" if platform == true
445
+ rv = [stringify_name(name, &block), stringify_version(version), !platform ? nil : (platform + @platform.to_s)].compact.flatten
446
+ join ? rv.join(join) : rv
447
+ end
448
+ alias :meta :classes
449
+
450
+ private
451
+ # Stringifies a browser name.
452
+ #
453
+ # @param name [Boolean] If non falsy, the string to prepend to the name. If falsy, the name information will not be included.
454
+ # @param block [Proc] A block to translate browser name.
455
+ # @return [String|nil] The browser name or `nil`, if it was set to be skipped
456
+ def stringify_name(name, &block)
457
+ name = "" if name == true
458
+ !name ? nil : "#{name}#{block_given? ? yield(@name, @version, @platform) : (@name || self.parse_agent(@agent))}"
340
459
  end
341
460
 
342
- # Replace a previous entry
343
- if index then
344
- @platforms[index] = platform
345
- else
346
- @platforms << platform
347
- @platforms_indexes[platform[0]] = @platforms.length - 1
461
+ # Stringifies a browser version.
462
+ #
463
+ # @param version [String|NilClass] If non falsy, the string to prepend to the version. If falsy, the version information will not be included.
464
+ # @return [Array] The version strings or `nil`, if it was set to be skipped
465
+ def stringify_version(version)
466
+ version = "version-" if version == true
467
+ others = @version.split(".")
468
+ major = others.shift
469
+ !version ? nil : others.inject([version + major]) {|prev, current| prev + [prev.last + "_" + current] }.flatten
348
470
  end
349
-
350
- rv = true
351
- end
352
-
353
- rv
354
471
  end
355
472
 
356
- # Registers a new language that can be recognized.
357
- #
358
- # @param code [String|Hash] The language code or an hash with codes as keys and label as values.
359
- # @param label [String] The language name. Ignored if code is an Hash.
360
- # @return [Boolean] `true` if at least one language has been added, `false` otherwise.
361
- def self.register_language(code, label = nil)
362
- @languages ||= {}
363
- rv = false
364
- code = {code.ensure_string => label.ensure_string} if !code.is_a?(Hash)
365
-
366
- code.each_pair do |c, l|
367
- if c.present? && l.present? then
368
- @languages[c] = l
369
- rv = true
370
- end
371
- end
473
+ # Methods to parse the user agent.
474
+ module Parsing
475
+ # Parses the User-Agent header.
476
+ # @param agent [String] The User-Agent header.
477
+ # @return [Boolean] `true` if the browser was detected, `false` otherwise.
478
+ def parse_agent(agent = nil)
479
+ agent = agent.ensure_string
372
480
 
373
- rv
374
- end
481
+ @name, version = match_name_and_version(agent)
482
+ @version = adjust_version(@version)
483
+ @platform = match_platform(agent)
375
484
 
376
- # Compares two versions.
377
- #
378
- # @param v1 [String] The first versions to compare.
379
- # @param operator [Symbol] The operator to use for comparison, can be one of `[:lt, :lte, :eq, :gte, :gt]`.
380
- # @param v2 [Symbol] The second version to compare.
381
- # @return [Boolean] true if comparison is valid, `false` otherwise.
382
- def self.compare_versions(v1 = "", operator = :eq, v2 = "")
383
- rv = true
384
-
385
- if [:lt, :lte, :eq, :gte, :gt].include?(operator) && v1.ensure_string.present? then
386
- # At first, split versions
387
- vv1 = v1.ensure_string.split(".").reverse
388
- vv2 = v2.ensure_string.split(".").reverse
389
-
390
- left = vv1.pop
391
- right = vv2.pop
392
-
393
- # Start comparison, picking from length
394
- rv = catch(:result) do
395
- while true do
396
- left = (left || "0").ensure_string
397
- right = (right || "0").ensure_string
398
-
399
- # Adjust for alpha, beta, etc
400
- if !left.is_integer? then
401
- ll = left.length
402
- left = right + left
403
- right = right + ("z" * ll)
404
- end
485
+ (@name != :unknown) ? true : false
486
+ end
405
487
 
406
- # Pad arguments to make correct string comparisons
407
- len = [left.length, right.length].max
408
- left = left.rjust(len, "0")
409
- right = right.rjust(len, "0")
410
-
411
- if left == right then # If we have still an equal version
412
- # Goto next tokens
413
- left = vv1.pop
414
- right = vv2.pop
415
-
416
- # We end the left version, so until now all tokens were equal. Check what to do.
417
- if left.blank? then
418
- case operator
419
- when :lt then throw(:result, right.present? && right.to_integer > 0) # The comparison is true if there is at least another right version number greater than 0.
420
- when :lte then throw(:result, right.blank? || right.is_integer?) # The comparison is true if there is no other right version or that is a integer (so it means no alpha, beta etc).
421
- when :eq then throw(:result, right.blank? || ([right] + vv2).uniq.first == "0") # The comparison is true if also right version ends or only zero are left.
422
- when :gte then throw(:result, right.blank? || !right.is_integer?) # The comparison is true if there is no other right version or that is not a integer (so it means alpha, beta etc).
423
- when :gt then throw(:result, right.present? && !right.is_integer?) # The comparison is true if there is at least another right version not a integer (so it means alpha, beta etc).
424
- end
425
- end
488
+ # Parses the Accept-Language header.
489
+ #
490
+ # @param accept_language [String] The Accept-Language header.
491
+ # @return [Array] The list of accepted languages.
492
+ def parse_accept_language(accept_language = nil)
493
+ accept_language.ensure_string.gsub(/;q=[\d.]+/, "").split(",").collect {|l| l.downcase.strip }.select{|l| l.present? }
494
+ end
426
495
 
427
- throw(:result, [:lte, :eq, :gte].include?(operator)) if left.blank? # If we end the left version, it means that all tokens were equal. Return accordingly
428
- else # We can compare a different token
429
- case operator
430
- when :lt, :lte then throw(:result, left < right)
431
- when :eq then throw(:result, false)
432
- when :gt, :gte then throw(:result, left > right)
496
+ private
497
+ # Matches a browser name and version.
498
+ #
499
+ # @param agent [String] The User-Agent header.
500
+ # @return [String|Symbol] The browser name or `:unknown`, if no match was found.
501
+ def match_name_and_version(agent)
502
+ catch(:name) do
503
+ ::Brauser::Browser.browsers.each do |name, definitions|
504
+ matched = match_definition(definitions[0], agent)
505
+
506
+ if matched then
507
+ @version = match_definition(definitions[1], name, agent)
508
+ throw(:name, name)
433
509
  end
434
510
  end
511
+
512
+ :unknown
435
513
  end
436
514
  end
437
- else
438
- rv = false
439
- end
440
-
441
- rv
442
- end
443
-
444
- # Creates a new browser.
445
- #
446
- # @param agent [String] The User-Agent HTTP header.
447
- # @param accept_language [String] The Accept-Language HTTP header.
448
- def initialize(agent = "", accept_language = "")
449
- self.class.register_default_browsers
450
- self.class.register_default_platforms
451
- self.class.register_default_languages
452
515
 
453
- @agent = agent
454
- @accept_language = accept_language
455
-
456
- @languages = self.parse_accept_language(@accept_language) if @accept_language
457
- self.parse_agent(@agent) if @agent
458
- end
459
-
460
- # Parses the User-Agent header.
461
- # @param agent [String] The User-Agent header.
462
- # @return [Boolean] `true` if the browser was detected, `false` otherwise.
463
- def parse_agent(agent = nil)
464
- agent = agent.ensure_string
465
-
466
- # At first, detect name and version. Tries order is important to avoid false positives.
467
- @name = catch(:name) do
468
- self.class.browsers.each do |name, definitions|
469
- matched = false
470
-
471
- if definitions[0].is_a?(::Regexp) then
472
- matched = definitions[0].match(agent) ? true : false
473
- elsif definitions[0].respond_to?(:call) then
474
- matched = (definitions[0].call(agent) ? true : false)
516
+ # Adjusts a browser version.
517
+ def adjust_version(version)
518
+ # Adjust version
519
+ if version.blank? then
520
+ version = "0.0"
521
+ elsif version.is_a?(::MatchData) then
522
+ version = version.to_a.last
475
523
  else
476
- matched = (agent == definitions[0].ensure_string)
524
+ version
477
525
  end
526
+ end
478
527
 
479
- if matched then # Found a match, go through version
480
- if definitions[1].is_a?(::Regexp) then
481
- @version = definitions[1].match(agent)
482
- @version = @version.to_a.last if @version.is_a?(::MatchData)
483
- elsif @version = definitions[1].respond_to?(:call) then
484
- @version = definitions[1].call(name, agent).ensure_string
485
- else
486
- @version = definitions[1].ensure_string
528
+ # Matches a browser platform.
529
+ #
530
+ # @param agent [String] The User-Agent header.
531
+ # @return [String|Symbol] The browser platform or `:unknown`, if no match was found.
532
+ def match_platform(agent)
533
+ catch(:platform) do
534
+ ::Brauser::Browser.platforms.each do |platform, definitions|
535
+ matched = match_definition(definitions[0], @name, agent)
536
+ throw(:platform, platform) if matched
487
537
  end
488
- end
489
538
 
490
- throw(:name, name) if matched
539
+ :unknown
540
+ end
491
541
  end
492
542
 
493
- :unknown
494
- end
495
-
496
- # Adjust version
497
- @version = "0.0" if @version.blank?
498
-
499
- # At last, detect platform
500
- @platform = catch(:platform) do
501
- self.class.platforms.each do |platform, definitions|
502
- if definitions[0].is_a?(::Regexp) then
503
- matched = definitions[0].match(agent) ? true : false
504
- elsif definitions[0].respond_to?(:call) then
505
- matched = (definitions[0].call(@name, agent) ? true : false)
543
+ # Matches a subject against a definition
544
+ #
545
+ # @param subject [Array] The subject to match.
546
+ # @param definition [StringRegexp|Block] The definition. If a block, it will be yielded with the subject must return `true` if the subject was recognized.
547
+ def match_definition(definition, *subject)
548
+ if definition.is_a?(::Regexp) then
549
+ definition.match(subject.last)
550
+ elsif definition.respond_to?(:call) then
551
+ definition.call(*subject)
506
552
  else
507
- matched = (agent == definitions[0].ensure_string)
553
+ subject.last == definition.ensure_string ? subject.last : nil
508
554
  end
509
-
510
- throw(:platform, platform) if matched
511
555
  end
512
-
513
- :unknown
514
- end
515
-
516
- (@name != :unknown) ? true : false
517
556
  end
518
557
 
519
- # Parses the Accept-Language header.
520
- #
521
- # @param accept_language [String] The Accept-Language header.
522
- # @return [Array] The list of accepted languages.
523
- def parse_accept_language(accept_language = nil)
524
- accept_language.ensure_string.gsub(/;q=[\d.]+/, "").split(",").collect {|l| l.downcase.strip }.select{|l| l.present? }
525
- end
558
+ # Methods to query with chaining.
559
+ module PartialQuerying
560
+ # Checks if the browser is a specific name and optionally of a specific version and platform.
561
+ #
562
+ # @see #v?
563
+ # @see #on?
564
+ #
565
+ # @param names [Symbol|Array] A list of specific names to match. Also, this meta-names are supported: `:capable` and `:tablet`.
566
+ # @param versions [String|Hash] A string in the form `operator version && ...` (example: `>= 7 && < 4`) or an hash with specific version to match against, in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
567
+ # @param platforms [Symbol|Array] A list of specific platform to match. Valid values are all those possible for the platform attribute.
568
+ # @return [Query] A query which can evaluated for concatenation or result.
569
+ def is(names = [], versions = {}, platforms = [])
570
+ self.parse_agent(@agent) if !@name
571
+
572
+ names = adjust_names(names)
573
+ versions = parse_versions_query(versions)
574
+ platforms = platforms.ensure_array
575
+
576
+ ::Brauser::Query.new(self,
577
+ (names.blank? || (names.include?(@name) && check_capable(names))) &&
578
+ (versions.blank? || self.v?(versions)) &&
579
+ (platforms.blank? || self.on?(platforms))
580
+ )
581
+ end
526
582
 
527
- # Gets a human-readable browser name.
528
- #
529
- # @return [String] A human-readable browser name.
530
- def readable_name
531
- self.parse_agent(@agent) if !@name
532
- self.class.browsers.fetch(@name, ["Unknown Browser"]).last.ensure_string
533
- end
583
+ # Checks if the brower is a specific version.
584
+ #
585
+ # @param versions [String|Hash] A string in the form `operator version && ...` (example: `>= 7 && < 4`) or an hash with specific version to match against, in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
586
+ # @return [Query] A query which can evaluated for concatenation or result.
587
+ def v(versions = {})
588
+ self.parse_agent(@agent) if !@version
589
+ rv = true
534
590
 
535
- # Gets a human-readable platform name.
536
- #
537
- # @return [String] A readable platform name.
538
- def platform_name
539
- self.parse_agent(@agent) if !@platform
540
- self.class.platforms.fetch(@platform, ["Unknown Platform"]).last.ensure_string
541
- end
591
+ versions = if versions.is_a?(String) then
592
+ parse_versions_query(versions)
593
+ elsif !versions.is_a?(::Hash) then
594
+ {}
595
+ else
596
+ versions
597
+ end
542
598
 
543
- # Checks if the browser is a specific name and optionally of a specific version and platform.
544
- #
545
- # @see #v?
546
- # @see #on?
547
- #
548
- # @param names [Symbol|Array] A list of specific names to match. Also, this meta-names are supported: `:capable` and `:tablet`.
549
- # @param versions [Hash] An hash with specific version to match against. Need to be in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
550
- # @param platforms [Symbol|Array] A list of specific platform to match. Valid values are all those possible for the platform attribute.
551
- # @return [Query] A query which can evaluated for concatenation or result.
552
- def is(names = [], versions = {}, platforms = [])
553
- self.parse_agent(@agent) if !@name
554
- versions = {} if !versions.is_a?(::Hash)
555
- platforms = platforms.ensure_array
556
-
557
- # Adjust names
558
- names = names.ensure_array.uniq.compact.collect {|n| n.ensure_string.to_sym }
559
- names << [:msie] if names.include?(:ie)
560
- names << [:chromium] if names.include?(:chrome)
561
-
562
- if names.delete(:capable) then
563
- names += [:chrome, :firefox, :safari, :opera, :msie]
564
-
565
- # Add version 9 as minimum for IE if capable is specified
566
- versions[:gte] = 9 if @name == :msie
599
+ ::Brauser::Query.new(self, versions.all? { |operator, value| Brauser::Browser.compare_versions(@version, operator, value) })
567
600
  end
568
601
 
569
- names << [:ipad, :android, :kindle] if names.delete(:tablet)
570
- names.compact!
602
+ # Check if the browser is on a specific platform.
603
+ #
604
+ # @param platforms [Symbol|Array] A list of specific platform to match.
605
+ # @return [Query] A query which can evaluated for concatenation or result.
606
+ def on(platforms = [])
607
+ self.parse_agent(@agent) if !@platform
608
+ ::Brauser::Query.new(self, platforms.blank? || platforms.ensure_array.uniq.compact.collect {|p| p.ensure_string.to_sym }.include?(@platform))
609
+ end
571
610
 
572
- rv = names.blank? || names.include?(@name)
611
+ # Check if the browser accepts the specified languages.
612
+ #
613
+ # @param langs [String|Array] A list of languages to match against.
614
+ # @return [Query] A query which can evaluated for concatenation or result.
615
+ def accepts(langs = [])
616
+ self.parse_accept_language(@accept_language) if !@languages
617
+ ::Brauser::Query.new(self, (@languages & langs.ensure_array.uniq.compact.collect {|l| l.to_s }).present?)
618
+ end
573
619
 
574
- # Look also for version
575
- rv = rv && self.v?(versions) if rv && versions.present?
620
+ private
621
+ # Adjusts names for correct matching.
622
+ #
623
+ # @param names [Array] A list of names.
624
+ # @return [Array] The adjusted list of names.
625
+ def adjust_names(names)
626
+ # Adjust names
627
+ names = names.ensure_array.compact.collect {|n| n.ensure_string.to_sym }
628
+ names << [:msie] if names.include?(:ie)
629
+ names << [:chromium] if names.include?(:chrome)
630
+ names << [:chrome, :firefox, :safari, :opera, :msie] if names.include?(:capable)
631
+ names << [:ipad, :android, :kindle] if names.include?(:tablet)
632
+ names.flatten.compact.uniq
633
+ end
576
634
 
577
- # Look also for platforms
578
- rv = rv && self.on?(platforms) if rv && platforms.present?
635
+ # Checks if the browser is capable.
636
+ #
637
+ # @param names [Array] A list of names.
638
+ # @return [Boolean] `true` if the browser is capable, `false` otherwise.
639
+ def check_capable(names)
640
+ !names.include?(:capable) || @name != :msie || Brauser::Browser.compare_versions(@version, :gte, 9)
641
+ end
579
642
 
580
- ::Brauser::Query.new(self, rv)
581
- end
643
+ # Parses a version query.
644
+ #
645
+ # @param versions [String|Hash] A string in the form `operator version && ...` (example: `>= 7 && < 4`) or an hash with specific version to match against, in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
646
+ # @return [Hash] The hash representation of the query.
647
+ def parse_versions_query(versions)
648
+ versions.is_a?(::Hash) ? versions : versions.ensure_string.split(/\s*&&\s*/).inject({}) do |prev, token|
649
+ operator, version = parse_versions_query_component(token)
650
+ prev[operator] = version if operator.present? && version.present?
651
+ prev
652
+ end
653
+ end
582
654
 
583
- # Checks if the browser is a specific name and optionally of a specific version and platform.
584
- #
585
- # @see #v?
586
- # @see #on?
587
- #
588
- # @param names [Symbol|Array] A list of specific names to match. Also, this meta-names are supported: `:capable` and `:tablet`.
589
- # @param versions [Hash] An hash with specific version to match against. Need to be in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
590
- # @param platforms [Symbol|Array] A list of specific platform to match. Valid values are all those possible for the platform attribute.
591
- # @return [Boolean] `true` if current browser matches, `false` otherwise.
592
- def is?(names = [], versions = {}, platforms = [])
593
- self.is(names, versions, platforms).result
655
+ # Parses a token of a version query.
656
+ #
657
+ # @param token [String] The token to parse.
658
+ # @return [Array] An operator and an argument.
659
+ def parse_versions_query_component(token)
660
+ operator, version = token.strip.split(/\s+/, 2).collect(&:strip)
661
+ [{"<" => :lt, "<=" => :lte, "=" => :eq, "==" => :eq, ">" => :gt, ">=" => :gte}.fetch(operator, nil), version]
662
+ end
594
663
  end
595
664
 
596
- # Checks if the brower is a specific version.
597
- #
598
- # @param versions [String|Hash] A string in the form `operator version && ...` (example: `>= 7 && < 4`) or an hash with specific version to match against, in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
599
- # @return [Query] A query which can evaluated for concatenation or result.
600
- def v(versions = {})
601
- self.parse_agent(@agent) if !@version
602
- rv = true
603
-
604
- if versions.is_a?(String) then
605
- versions_s = versions
606
- versions = {}
607
-
608
- versions_s.split(/\s*&&\s*/).each do |token|
609
- next if token.strip.empty?
610
- tokens = token.strip.split(/\s+/).collect {|t| t.strip}
611
- operator = case tokens[0]
612
- when "<" then :lt
613
- when "<=" then :lte
614
- when "=" then :eq
615
- when "==" then :eq
616
- when ">=" then :gte
617
- when ">" then :gt
618
- else tokens[0].downcase.to_sym
619
- end
665
+ # Methods to end querying.
666
+ module Querying
667
+ # Checks if the browser is a specific name and optionally of a specific version and platform.
668
+ #
669
+ # @see #v?
670
+ # @see #on?
671
+ #
672
+ # @param names [Symbol|Array] A list of specific names to match. Also, this meta-names are supported: `:capable` and `:tablet`.
673
+ # @param versions [Hash] An hash with specific version to match against. Need to be in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
674
+ # @param platforms [Symbol|Array] A list of specific platform to match. Valid values are all those possible for the platform attribute.
675
+ # @return [Boolean] `true` if current browser matches, `false` otherwise.
676
+ def is?(names = [], versions = {}, platforms = [])
677
+ self.is(names, versions, platforms).result
678
+ end
620
679
 
621
- versions[operator] = tokens[1]
622
- end
623
- else
624
- versions = {} if !versions.is_a?(::Hash)
680
+ # Checks if the brower is a specific version.
681
+ #
682
+ # @param versions [String|Hash] A string in the form `operator version && ...` (example: `>= 7 && < 4`) or an hash with specific version to match against, in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
683
+ # @return [Boolean] `true` if current browser matches, `false` otherwise.
684
+ def v?(versions = {})
685
+ self.v(versions).result
625
686
  end
626
687
 
627
- versions.each do |operator, value|
628
- value = value.ensure_string
629
- rv = rv && Brauser::Browser.compare_versions(@version, operator, value)
630
- break if !rv
688
+ # Check if the browser is on a specific platform.
689
+ #
690
+ # @param platforms [Symbol|Array] A list of specific platform to match.
691
+ # @return [Boolean] `true` if current browser matches, `false` otherwise.
692
+ def on?(platforms = [])
693
+ self.on(platforms).result
631
694
  end
632
695
 
633
- ::Brauser::Query.new(self, rv)
696
+ # Check if the browser accepts the specified languages.
697
+ #
698
+ # @param langs [String|Array] A list of languages to match against.
699
+ # @return [Boolean] `true` if current browser matches, `false` otherwise.
700
+ def accepts?(langs = [])
701
+ self.accepts(langs).result
702
+ end
634
703
  end
704
+ end
635
705
 
636
- # Checks if the brower is a specific version.
637
- #
638
- # @param versions [String|Hash] A string in the form `operator version && ...` (example: `>= 7 && < 4`) or an hash with specific version to match against, in form `{:operator => version}`, where operator is one of `:lt, :lte, :eq, :gt, :gte`.
639
- # @return [Boolean] `true` if current browser matches, `false` otherwise.
640
- def v?(versions = {})
641
- self.v(versions).result
642
- end
706
+ # This class represents a detection of the current user browser.
707
+ #
708
+ # @attribute agent
709
+ # @return [String] The raw User-Agent HTTP header.
710
+ # @attribute accept_language
711
+ # @return [String] The raw Accept-Language HTTP header.
712
+ # @attribute languages
713
+ # @return [Array] The accepted languages.
714
+ # @attribute name
715
+ # @return [String] The current browser name.
716
+ # @attribute version
717
+ # @return [String] The current browser version.
718
+ # @attribute platform
719
+ # @return [String] The current browser platform.
720
+ class Browser
721
+ attr_accessor :agent
722
+ attr_accessor :accept_language
723
+ attr_accessor :languages
724
+ attr_accessor :name
725
+ attr_accessor :version
726
+ attr_accessor :platform
643
727
 
644
- # Check if the browser is on a specific platform.
645
- #
646
- # @param platforms [Symbol|Array] A list of specific platform to match.
647
- # @return [Query] A query which can evaluated for concatenation or result.
648
- def on(platforms = [])
649
- self.parse_agent(@agent) if !@platform
650
- ::Brauser::Query.new(self, platforms.blank? || platforms.ensure_array.uniq.compact.collect {|p| p.ensure_string.to_sym }.include?(@platform))
651
- end
728
+ # Aliases
729
+ alias :ua :agent
730
+ alias :ua= :agent=
652
731
 
653
- # Check if the browser is on a specific platform.
654
- #
655
- # @param platforms [Symbol|Array] A list of specific platform to match.
656
- # @return [Boolean] `true` if current browser matches, `false` otherwise.
657
- def on?(platforms = [])
658
- self.on(platforms).result
659
- end
732
+ include ::Brauser::BrowserMethods::General
733
+ include ::Brauser::BrowserMethods::Attributes
734
+ include ::Brauser::BrowserMethods::Register
735
+ include ::Brauser::BrowserMethods::Parsing
736
+ include ::Brauser::BrowserMethods::PartialQuerying
737
+ include ::Brauser::BrowserMethods::Querying
660
738
 
661
- # Check if the browser accepts the specified languages.
739
+ # Creates a new browser.
662
740
  #
663
- # @param langs [String|Array] A list of languages to match against.
664
- # @return [Query] A query which can evaluated for concatenation or result.
665
- def accepts(langs = [])
666
- self.parse_accept_language(@accept_language) if !@languages
667
- ::Brauser::Query.new(self, (@languages & langs.ensure_array.uniq.compact.collect {|l| l.to_s }).present?)
668
- end
741
+ # @param agent [String] The User-Agent HTTP header.
742
+ # @param accept_language [String] The Accept-Language HTTP header.
743
+ def initialize(agent = "", accept_language = "")
744
+ ::Brauser::Browser.register_default_browsers
745
+ ::Brauser::Browser.register_default_platforms
746
+ ::Brauser::Browser.register_default_languages
669
747
 
670
- # Check if the browser accepts the specified languages.
671
- #
672
- # @param langs [String|Array] A list of languages to match against.
673
- # @return [Boolean] `true` if current browser matches, `false` otherwise.
674
- def accepts?(langs = [])
675
- self.accepts(langs).result
748
+ @agent = agent
749
+ @accept_language = accept_language
750
+
751
+ @languages = self.parse_accept_language(@accept_language) if @accept_language
752
+ self.parse_agent(@agent) if @agent
676
753
  end
677
754
 
678
755
  # This method enables the use of dynamic queries in just one method.
@@ -693,102 +770,76 @@ module Brauser
693
770
  # @param block [Proc] A block to pass to the method. Unused from the query.
694
771
  # @return [Boolean|Query|nil] A query or a boolean value (if `method` ends with `?`). If the query is not valid, `NoMethodError` will be raised.
695
772
  def method_missing(query, *arguments, &block)
696
- rv = nil
697
- oq = query
698
- oa = arguments
699
-
700
- query = query.ensure_string
701
-
702
- # See if return a boolean in case of valid query
703
- as_result = query =~ /\?$/
704
- query = query.gsub(/\?$/, "")
705
-
706
- # Parse the query
707
- query.ensure_string.split("__").each do |parts|
708
- tokens = parts.split("_")
709
- method = tokens[0]
710
- arguments = tokens[1, tokens.length].join("_")
711
-
712
- if method == "v" then
713
- arguments.gsub("_", ".")
714
- end
715
-
716
- if ["is", "v", "on"].include?(method) then
717
- if method == "v" then
718
- arguments = arguments.gsub(/_?eq_?/, " == ") # Parse ==
719
- arguments = arguments.gsub(/_?lte_?/, " <= ").gsub(/_?gte_?/, " >= ") # Parse <= and >=
720
- arguments = arguments.gsub(/_?lt_?/, " < ").gsub(/_?gt_?/, " > ") # Parse < and >
721
- arguments = arguments.gsub(/_?and_?/, " && ") # Parse &&
722
- arguments = arguments.gsub("_", ".").gsub(/\s+/, " ").strip # Replace _ and spaces, then strip
723
- end
724
-
725
- rv = Brauser::Query.new(self, true) if !rv
726
- rv = rv.send(method, *arguments)
727
-
728
- break if rv.result == false
729
- else # Invalid finder
730
- rv = nil
731
- break
732
- end
773
+ begin
774
+ parsed_query = parse_query(query.ensure_string)
775
+ rv = execute_query(parsed_query) || Brauser::Query.new(self, false)
776
+ query.ensure_string =~ /\?$/ ? rv.result : rv
777
+ rescue NoMethodError
778
+ super(query, *arguments, &block)
733
779
  end
734
-
735
- rv = rv.result if !rv.nil? && as_result
736
- !rv.nil? ? rv : super(oq, *oa, &block)
737
780
  end
738
781
 
739
- # Returns an array of information about the browser. Information are strings which are suitable to use as CSS classes.
740
- #
741
- # For version, it will be included a class for every token of the version. For example, version `7.0.1.2` will return this:
742
- #
743
- # ```ruby
744
- # ["version-7", "version-7_0", "version-7_0_1", "version-7_0_1_2"]
745
- # ```
782
+ # Returns the current browser as a string.
746
783
  #
747
- # If you provide a block (with accepts name, version and platform as arguments), it will be used for translating the name.
784
+ # @see #classes
748
785
  #
749
- # @param join [String|NilClass] If non falsy, the separator to use to join information. If falsy, informations will be returned as array.
750
- # @param name [Boolean] If non falsy, the string to prepend to the name. If falsy, the name information will not be included.
751
- # @param version [String|NilClass] If non falsy, the string to prepend to the version. If falsy, the version information will not be included.
752
- # @param platform [String|NilClass] If non falsy, the string to prepend to the platform. If falsy, the platform information will not be included.
753
- # @return [String|Array] CSS ready information of the current browser.
754
- def classes(join = " ", name = "", version = "version-", platform = "platform-")
755
- self.parse_agent(@agent) if !@name
756
- rv = []
757
- name = "" if name == true
758
- version = "version-" if version == true
759
- platform = "platform-" if platform == true
760
-
761
- # Manage name
762
- if name then
763
- final_name = block_given? ? yield(@name, @version, @platform) : @name
764
- rv << name + final_name.ensure_string
786
+ # @return [String] A string representation of the current browser.
787
+ def to_s
788
+ self.classes
789
+ end
790
+
791
+ private
792
+ # Parse query, getting all arguments.
793
+ # @param query [String] The query to issue. Use `__` to separate query and `_` in place of `.` in the version.
794
+ # @return [Array] And array of `[method, arguments]` entries.
795
+ def parse_query(query)
796
+ query.gsub(/\?$/, "").split("__").collect do |part|
797
+ parse_query_part(part)
798
+ end
765
799
  end
766
800
 
767
- # Manage platform
768
- if version then
769
- vbuffer = []
770
- vtokens = @version.split(".")
801
+ # Handles a part of a query.
802
+ #
803
+ # @param part [String] A part of a query.
804
+ # @return [Boolean|Query|nil] A query or a boolean value (if `method` ends with `?`). If the query is not valid, `NoMethodError` will be raised.
805
+ def parse_query_part(part)
806
+ method, arguments = part.split("_", 2)
771
807
 
772
- vtokens.each do |v|
773
- vbuffer << v
774
- rv << (version + vbuffer.join("_"))
808
+ if method == "v" then
809
+ arguments = parse_query_version(arguments)
810
+ elsif !["is", "on"].include?(method)
811
+ raise NoMethodError
775
812
  end
776
- end
777
813
 
778
- rv << (platform + @platform.to_s) if platform
814
+ [method, arguments]
815
+ end
779
816
 
780
- # Return
781
- join ? rv.join(join) : rv
782
- end
783
- alias :meta :classes
817
+ # Parses the version for a query.
818
+ #
819
+ # @param version [String] The version to parse.
820
+ # @return [String] The parsed version.
821
+ def parse_query_version(version)
822
+ [
823
+ [/_?eq_?/, " == "], # Parse ==
824
+ [/_?lte_?/, " <= "], # Parse <=
825
+ [/_?gte_?/, " >= "], # Parse >=
826
+ [/_?lt_?/, " < "], # Parse <
827
+ [/_?gt_?/, " > "], # Parse >
828
+ [/_?and_?/, " && "], # Parse &&
829
+ ["_", "."], # Dot notation
830
+ [/\s+/, " "]
831
+ ].inject(version) { |current, parse| current.gsub(parse[0], parse[1])}.strip
832
+ end
784
833
 
785
- # Returns the current browser as a string.
786
- #
787
- # @see #classes
788
- #
789
- # @return [String] A string representation of the current browser.
790
- def to_s
791
- self.classes
792
- end
834
+ # Executes a parsed query
835
+ #
836
+ # @param query [Array] And array of `[method, arguments]` entries.
837
+ # @return [Brauser::Query] The result of the query.
838
+ def execute_query(query)
839
+ query.inject(Brauser::Query.new(self, true)) { |rv, call|
840
+ break if !rv.result
841
+ rv.send(call[0], *call[1])
842
+ }
843
+ end
793
844
  end
794
845
  end