brauser 1.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.
data/lib/brauser.rb ADDED
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the brauser gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ require "lazier"
8
+
9
+ Lazier.load!(:object)
10
+
11
+ require "brauser/version" if !defined?(Brauser::Version)
12
+ require "brauser/query"
13
+ require "brauser/browser"
14
+ require "brauser/hooks"
@@ -0,0 +1,794 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the brauser gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ # A framework agnostic browser detection and querying helper.
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
25
+
26
+ # The current browser platform.
27
+ attr_accessor :platform
28
+
29
+ # Aliases
30
+ alias :ua :agent
31
+ alias :ua= :agent=
32
+
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
40
+
41
+ @browsers.each do |browser|
42
+ rv[browser[0]] = browser[1, browser.length]
43
+ end
44
+
45
+ rv
46
+ end
47
+
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
55
+
56
+ @platforms.each do |platform|
57
+ rv[platform[0]] = platform[1, platform.length]
58
+ end
59
+
60
+ rv
61
+ end
62
+
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
71
+
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(".")
116
+ end
117
+
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"],
123
+
124
+ [:webkit, /webkit/i, /(.+WebKit\/)([a-z0-9.]+)/i, "WebKit Browser"],
125
+ [:gecko, /gecko/i, /(.+rv:|Gecko\/)([a-z0-9.]+)/i, "Gecko Browser"],
126
+ ])
127
+
128
+ @browsers.present? ? true : false
129
+ end
130
+
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
155
+
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
281
+
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
294
+
295
+ rv = false
296
+ name = [[name.ensure_string.to_sym, name_match, version_match, label]] if !name.is_a?(Array)
297
+
298
+ name.each do |browser|
299
+ browser[0] = browser[0].to_sym
300
+
301
+ index = @browsers.index do |item|
302
+ item[0] == browser[0]
303
+ end
304
+
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
312
+
313
+ rv = true
314
+ end
315
+
316
+ rv
317
+ end
318
+
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 = {}
329
+ end
330
+
331
+
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]
340
+ end
341
+
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
348
+ end
349
+
350
+ rv = true
351
+ end
352
+
353
+ rv
354
+ end
355
+
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
372
+
373
+ rv
374
+ end
375
+
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
405
+
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
426
+
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)
433
+ end
434
+ end
435
+ end
436
+ 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
+
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)
475
+ else
476
+ matched = (agent == definitions[0].ensure_string)
477
+ end
478
+
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
487
+ end
488
+ end
489
+
490
+ throw(:name, name) if matched
491
+ end
492
+
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)
506
+ else
507
+ matched = (agent == definitions[0].ensure_string)
508
+ end
509
+
510
+ throw(:platform, platform) if matched
511
+ end
512
+
513
+ :unknown
514
+ end
515
+
516
+ (@name != :unknown) ? true : false
517
+ end
518
+
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
526
+
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
534
+
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
542
+
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
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
567
+ end
568
+
569
+ names << [:ipad, :android, :kindle] if names.delete(:tablet)
570
+ names.compact!
571
+
572
+ rv = names.blank? || names.include?(@name)
573
+
574
+ # Look also for version
575
+ rv = rv && self.v?(versions) if rv && versions.present?
576
+
577
+ # Look also for platforms
578
+ rv = rv && self.on?(platforms) if rv && platforms.present?
579
+
580
+ ::Brauser::Query.new(self, rv)
581
+ end
582
+
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
594
+ end
595
+
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
620
+
621
+ versions[operator] = tokens[1]
622
+ end
623
+ else
624
+ versions = {} if !versions.is_a?(::Hash)
625
+ end
626
+
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
631
+ end
632
+
633
+ ::Brauser::Query.new(self, rv)
634
+ end
635
+
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
643
+
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.collect {|p| p.ensure_string.to_sym }.include?(@platform))
651
+ end
652
+
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
660
+
661
+ # Check if the browser accepts the specified languages.
662
+ #
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).present?)
668
+ end
669
+
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
676
+ end
677
+
678
+ # This method enables the use of dynamic queries in just one method.
679
+ #
680
+ # For example:
681
+ #
682
+ # ```ruby
683
+ # browser.is_msie_gt_4_1__on_windows?
684
+ # #=> true
685
+ # ```
686
+ #
687
+ # If you don't provide a trailing `?`, you will get a Brauser::Query.
688
+ #
689
+ # If the syntax is invalid, a `NoMethodError` exception will be raised.
690
+ #
691
+ # @param query [String] The query to issue. Use `__` to separate query and `_` in place of `.` in the version.
692
+ # @param arguments [Array] The arguments to pass the method. Unused from the query.
693
+ # @param block [Proc] A block to pass to the method. Unused from the query.
694
+ # @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
+ 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
733
+ end
734
+
735
+ rv = rv.result if !rv.nil? && as_result
736
+ !rv.nil? ? rv : super(oq, *oa, &block)
737
+ end
738
+
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
+ # ```
746
+ #
747
+ # If you provide a block (with accepts name, version and platform as arguments), it will be used for translating the name.
748
+ #
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
765
+ end
766
+
767
+ # Manage platform
768
+ if version then
769
+ vbuffer = []
770
+ vtokens = @version.split(".")
771
+
772
+ vtokens.each do |v|
773
+ vbuffer << v
774
+ rv << (version + vbuffer.join("_"))
775
+ end
776
+ end
777
+
778
+ rv << (platform + @platform.to_s) if platform
779
+
780
+ # Return
781
+ join ? rv.join(join) : rv
782
+ end
783
+ alias :meta :classes
784
+
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
793
+ end
794
+ end