brauser 1.0.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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