# encoding: utf-8 # # This file is part of the brauser gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>. # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php. # # A framework agnostic browser detection and querying helper. module Brauser # Methods of the {Browser Browser} class. module BrowserMethods # Methods for register recognized browsers, versions and platforms. module Register extend ActiveSupport::Concern # Class methods. module ClassMethods # Registers the default list of browsers that can be recognized. # # @return [Boolean] `true` if at least one browser has been added, `false` otherwise. def register_default_browsers @browsers = nil register_mobile_browsers register_desktop_browsers @browsers.present? ? true : false end # Registers the default list of platforms that can be recognized. # # @return [Boolean] `true` if at least one platform has been added, `false` otherwise. def register_default_platforms @platforms = nil self.register_platform([ [:symbian, /s60|symb/i, "Symbian"], [:windows_phone, /windows phone/i, "Microsoft Windows Phone"], [:kindle, Proc.new { |name, agent| name == :kindle }, "Nokia Symbian"], [:ios, Proc.new { |name, agent| [:iphone, :ipad, :ipod].include?(name) || agent =~ /ipad|iphone|ipod/i }, "Apple iOS"], [:android, /android/i, "Android"], [:blackberry, /blackberry/i, "RIM BlackBerry"], [:psp, /psp/i, "Sony Playstation Portable"], [:ps3, /playstation 3/i, "Sony Playstation 3"], [:wii, /wii/i, "Nintendo Wii"], [:linux, /linux/i, "Linux"], [:osx, /mac|macintosh|mac os x/i, "Apple MacOS X"], [:windows, /windows/i, "Microsoft Windows"] ]) @platforms.present? ? true : false end # Registers the default list of languages that can be recognized. # # @return [Boolean] `true` if at least one language has been added, `false` otherwise. def register_default_languages @languages = nil self.register_language({ "af" => "Afrikaans", "sq" => "Albanian", "eu" => "Basque", "bg" => "Bulgarian", "be" => "Byelorussian", "ca" => "Catalan", "zh" => "Chinese", "zh-cn" => "Chinese/China", "zh-tw" => "Chinese/Taiwan", "zh-hk" => "Chinese/Hong Kong", "zh-sg" => "Chinese/singapore", "hr" => "Croatian", "cs" => "Czech", "da" => "Danish", "nl" => "Dutch", "nl-nl" => "Dutch/Netherlands", "nl-be" => "Dutch/Belgium", "en" => "English", "en-gb" => "English/United Kingdom", "en-us" => "English/United States", "en-au" => "English/Australian", "en-ca" => "English/Canada", "en-nz" => "English/New Zealand", "en-ie" => "English/Ireland", "en-za" => "English/South Africa", "en-jm" => "English/Jamaica", "en-bz" => "English/Belize", "en-tt" => "English/Trinidad", "et" => "Estonian", "fo" => "Faeroese", "fa" => "Farsi", "fi" => "Finnish", "fr" => "French", "fr-be" => "French/Belgium", "fr-fr" => "French/France", "fr-ch" => "French/Switzerland", "fr-ca" => "French/Canada", "fr-lu" => "French/Luxembourg", "gd" => "Gaelic", "gl" => "Galician", "de" => "German", "de-at" => "German/Austria", "de-de" => "German/Germany", "de-ch" => "German/Switzerland", "de-lu" => "German/Luxembourg", "de-li" => "German/Liechtenstein", "el" => "Greek", "he" => "Hebrew", "he-il" => "Hebrew/Israel", "hi" => "Hindi", "hu" => "Hungarian", "ie-ee" => "Internet Explorer/Easter Egg", "is" => "Icelandic", "id" => "Indonesian", "in" => "Indonesian", "ga" => "Irish", "it" => "Italian", "it-ch" => "Italian/ Switzerland", "ja" => "Japanese", "km" => "Khmer", "km-kh" => "Khmer/Cambodia", "ko" => "Korean", "lv" => "Latvian", "lt" => "Lithuanian", "mk" => "Macedonian", "ms" => "Malaysian", "mt" => "Maltese", "no" => "Norwegian", "pl" => "Polish", "pt" => "Portuguese", "pt-br" => "Portuguese/Brazil", "rm" => "Rhaeto-Romanic", "ro" => "Romanian", "ro-mo" => "Romanian/Moldavia", "ru" => "Russian", "ru-mo" => "Russian /Moldavia", "gd" => "Scots Gaelic", "sr" => "Serbian", "sk" => "Slovack", "sl" => "Slovenian", "sb" => "Sorbian", "es" => "Spanish", "es-do" => "Spanish", "es-ar" => "Spanish/Argentina", "es-co" => "Spanish/Colombia", "es-mx" => "Spanish/Mexico", "es-es" => "Spanish/Spain", "es-gt" => "Spanish/Guatemala", "es-cr" => "Spanish/Costa Rica", "es-pa" => "Spanish/Panama", "es-ve" => "Spanish/Venezuela", "es-pe" => "Spanish/Peru", "es-ec" => "Spanish/Ecuador", "es-cl" => "Spanish/Chile", "es-uy" => "Spanish/Uruguay", "es-py" => "Spanish/Paraguay", "es-bo" => "Spanish/Bolivia", "es-sv" => "Spanish/El salvador", "es-hn" => "Spanish/Honduras", "es-ni" => "Spanish/Nicaragua", "es-pr" => "Spanish/Puerto Rico", "sx" => "Sutu", "sv" => "Swedish", "sv-se" => "Swedish/Sweden", "sv-fi" => "Swedish/Finland", "ts" => "Thai", "tn" => "Tswana", "tr" => "Turkish", "uk" => "Ukrainian", "ur" => "Urdu", "vi" => "Vietnamese", "xh" => "Xshosa", "ji" => "Yiddish", "zu" => "Zulu" }) @languages.present? ? true : false end # Registers a new browser that can be recognized. # # @param name [Symbol|Array] The browser name or a list of browser (a list of array with `[name, name_match, version_match, label]` entries). # @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. # @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. # @param label [String] A human readable name of the browser. # @return [Boolean] `true` if at least one browser has been added, `false` otherwise. def register_browser(name, name_match = nil, version_match = nil, label = nil) @browsers ||= [] register_entries(@browsers, (name.is_a?(Array) ? name : [[name.ensure_string, name_match, version_match, label]])) end # Registers a new platform that can be recognized. # # @param name [Symbol|Array] The platform name or a list of platforms (a list of array with `[name, matcher, label]` entries). # @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. # @param label [String] A human readable name of the platform. # @return [Boolean] `true` if at least one platform has been added, `false` otherwise. def register_platform(name, matcher = nil, label = nil) @platforms ||= [] register_entries(@platforms, (name.is_a?(Array) ? name : [[name.ensure_string, matcher, label]])) end # Registers a new language that can be recognized. # # @param code [String|Hash] The language code or an hash with codes as keys and label as values. # @param label [String] The language name. Ignored if code is an Hash. # @return [Boolean] `true` if at least one language has been added, `false` otherwise. def register_language(code, label = nil) @languages ||= {} rv = false code = {code.ensure_string => label.ensure_string} if !code.is_a?(Hash) code.each_pair do |c, l| if c.present? && l.present? then @languages[c] = l rv = true end end rv end private # Register the most common desktop browsers. # @return [Boolean] `true` if at least one browser has been added, `false` otherwise. def register_desktop_browsers self.register_browser([ [:chrome, /((chrome)|(chromium))/i, /(.+Chrom[a-z]+\/)([a-z0-9.]+)/i, "Google Chrome"], [:netscape, /(netscape|navigator)\//i, /((Netscape|Navigator)\/)([a-z0-9.]+)/i, "Netscape Navigator"], [:firefox, /firefox/i, /(.+Firefox\/)([a-z0-9.]+)/i, "Mozilla Firefox"], [:safari, Proc.new{ |agent| agent =~ /safari/i && agent !~ /((chrome)|(chromium))/i }, /(.+Version\/)([a-z0-9.]+)/i, "Apple Safari"], [:msie_compatibility, /trident/i, Proc.new { |name, agent| version = /(.+Trident\/)([a-z0-9.]+)/i.match(agent) if version.is_a?(::MatchData) then v = version.to_a.last.split(".") v[0] = v[0].to_integer + 4 version = v.join(".") end version }, "Microsoft Internet Explorer (Compatibility View)"], [:msie, Proc.new{ |agent| agent =~ /msie/i && agent !~ /opera/i }, /(.+MSIE )([a-z0-9.]+)/i, "Microsoft Internet Explorer"], [:quicktime, /quicktime/i, /(.+((QuickTime\/)|(qtver=)))([a-z0-9.]+)/i, "Apple QuickTime"], [:webkit, /webkit/i, /(.+WebKit\/)([a-z0-9.]+)/i, "WebKit Browser"], [:gecko, /gecko/i, /(.+rv:|Gecko\/)([a-z0-9.]+)/i, "Gecko Browser"], ]) end # Register the most common mobile and console browsers. # @return [Boolean] `true` if at least one browser has been added, `false` otherwise. def register_mobile_browsers self.register_browser([ [:coremedia, /coremedia/i, /.+CoreMedia v([a-z0-9.]+)/i, "Apple CoreMedia"], [:opera_mobile, /opera mobi/i, /.+Opera Mobi.+((.+Opera )|(Version\/))([a-z0-9.]+)/i, "Opera Mobile"], [:opera, /opera/i, Proc.new{ |name, agent| regexp = (agent !~ /wii/i) ? /((.+Opera )|(Version\/))([a-z0-9.]+)/i : /(.+Nintendo Wii; U; ; )([a-z0-9.]+)/i version = regexp.match(agent) version = version.to_a.last if version.is_a?(MatchData) version }, "Opera"], [:android, /android/i, /(.+Android )([a-z0-9.]+)/i, "Android"], [:blackberry, /blackberry/i, /(.+Version\/)([a-z0-9.]+)/i, "RIM BlackBerry"], [:kindle, /(kindle)/i, /(.+(Kindle|Silk)\/)([a-z0-9.]+)/i, "Amazon Kindle"], [:psp, /psp/i, /(.+PlayStation Portable\); )([a-z0-9.]+)/i, "Sony Playstation Portable"], [:ps3, /playstation 3/i, /(.+PLAYSTATION 3; )([a-z0-9.]+)/i, "Sony Playstation 3"], [:windows_phone, /windows phone/i, /(.+IEMobile\/)([a-z0-9.]+)/i, "Microsoft Windows Phone"], [:wii, /nintendo wii/, /(.+Nintendo Wii; U; ; )([a-z0-9.]+)/i, "Nintendo Wii"], [:ipod, /ipod/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPod"], [:iphone, /iphone/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPhone"], [:ipad, /ipad/i, /(.+Version\/)([a-z0-9.]+)/i, "Apple iPad"], [:mobile, /(mobile|symbian|midp|windows ce)/i, /.+\/([a-z0-9.]+)/i, "Other Mobile Browser"], ]) end # Registers a new set of entries to a collection. # # @param collection [Array] The collection which add entries to. # @param entries [Array] The entries to add. def register_entries(collection, entries) rv = false entries.each do |entry| entry[0] = entry[0].to_sym index = collection.find_index { |item| item[0] == entry[0] } # Replace a previous entry if index then collection[index] = entry else collection << entry rv = true end end rv end end end # General methods. module General extend ActiveSupport::Concern # Class methods. module ClassMethods # Returns the list of browser that can be recognized. # # The keys are the browser name, the values are arrays of the name matcher, the version match and the label. # # @return [Hash] The list of browser that can be recognized. def browsers registered_to_hash(@browsers) end # Returns the list of platforms that can be recognized. # # The keys are the platform name, values are arrays of the matcher and the label. # # @return [Hash] The list of platform that can be recognized. def platforms registered_to_hash(@platforms) end # Returns the list of languages that can be recognized. # # The keys are the languages code, the values the labels. # # @return [Hash] The list of languages that can be recognized. def languages @languages end # Compares two versions. # # @param v1 [String] The first versions to compare. # @param operator [Symbol] The operator to use for comparison, can be one of `[:lt, :lte, :eq, :gte, :gt]`. # @param v2 [Symbol] The second version to compare. # @return [Boolean] true if comparison is valid, `false` otherwise. def compare_versions(v1 = "", operator = :eq, v2 = "") valid_results = {lt: [-1], lte: [-1, 0], eq: [0], gte: [0, 1], gt: [1]}.fetch(operator, []) if valid_results.present? && v1.ensure_string.present? then p1, p2 = find_relevant_tokens(v1.ensure_string.strip, v2.ensure_string.strip) p1, p2 = normalize_tokens(p1, p2) valid_results.include?(p1 <=> p2) else false end end private # Find relevant tokens (that is, the first two which are not equals) in a string for comparison. # # @param v1 [String] The first versions to compare. # @param v2 [String] The second version to compare. # @return [Array] The tokens to compare. def find_relevant_tokens(v1, v2) v1 = v1.split(".") v2 = v2.split(".") p1 = nil p2 = nil [v1.length, v2.length].max.times do |i| p1 = v1[i] p2 = v2[i] break if !p1 && !p2 || p1 != p2 end [p1 || "0", p2 || "0"] end # Normalizes token for comparison. # # @param p1 [String] The first token to normalize. # @param p2 [String] The second token to normalize. # @return [Array] The tokens to compare. def normalize_tokens(p1, p2) if !p1.is_integer? then ll = p1.length p1 = p2 + p1 p2 = p2 + ("z" * ll) end [p1, p2] end # Converts a list of register entries to an ordered hash. # # @param entries [Array] The array to convert. # @return [OrderedHash] An ordered hash. def registered_to_hash(entries) entries.inject(ActiveSupport::OrderedHash.new) do |rv, entry| rv[entry[0]] = entry[1, entry.length] rv end end end end # Methods to handle attributes module Attributes # Gets a human-readable browser name. # # @return [String] A human-readable browser name. def readable_name self.parse_agent(@agent) if !@name ::Brauser::Browser.browsers.fetch(@name, ["Unknown Browser"]).last.ensure_string end # Gets a human-readable platform name. # # @return [String] A readable platform name. def platform_name self.parse_agent(@agent) if !@platform ::Brauser::Browser.platforms.fetch(@platform, ["Unknown Platform"]).last.ensure_string end # Returns an array of information about the browser. Information are strings which are suitable to use as CSS classes. # # For version, it will be included a class for every token of the version. For example, version `7.0.1.2` will return this: # # ```ruby # ["version-7", "version-7_0", "version-7_0_1", "version-7_0_1_2"] # ``` # # If you provide a block (with accepts name, version and platform as arguments), it will be used for translating the name. # # @param join [String|NilClass] If non falsy, the separator to use to join information. If falsy, informations will be returned as array. # @param name [Boolean] If non falsy, the string to prepend to the name. If falsy, the name information will not be included. # @param version [String|NilClass] If non falsy, the string to prepend to the version. If falsy, the version information will not be included. # @param platform [String|NilClass] If non falsy, the string to prepend to the platform. If falsy, the platform information will not be included. # @param block [Proc] A block to translate browser name. # @return [String|Array] CSS ready information of the current browser. def classes(join = " ", name = "", version = "version-", platform = "platform-", &block) platform = "platform-" if platform == true rv = [stringify_name(name, &block), stringify_version(version), !platform ? nil : (platform + @platform.to_s)].compact.flatten join ? rv.join(join) : rv end alias :meta :classes private # Stringifies a browser name. # # @param name [Boolean] If non falsy, the string to prepend to the name. If falsy, the name information will not be included. # @param block [Proc] A block to translate browser name. # @return [String|nil] The browser name or `nil`, if it was set to be skipped def stringify_name(name, &block) name = "" if name == true !name ? nil : "#{name}#{block_given? ? yield(@name, @version, @platform) : (@name || self.parse_agent(@agent))}" end # Stringifies a browser version. # # @param version [String|NilClass] If non falsy, the string to prepend to the version. If falsy, the version information will not be included. # @return [Array] The version strings or `nil`, if it was set to be skipped def stringify_version(version) version = "version-" if version == true others = @version.split(".") major = others.shift !version ? nil : others.inject([version + major]) {|prev, current| prev + [prev.last + "_" + current] }.flatten end end # Methods to parse the user agent. module Parsing # Parses the User-Agent header. # @param agent [String] The User-Agent header. # @return [Boolean] `true` if the browser was detected, `false` otherwise. def parse_agent(agent = nil) agent = agent.ensure_string @name, version = match_name_and_version(agent) @version = adjust_version(@version) @platform = match_platform(agent) (@name != :unknown) ? true : false end # Parses the Accept-Language header. # # @param accept_language [String] The Accept-Language header. # @return [Array] The list of accepted languages. def parse_accept_language(accept_language = nil) accept_language.ensure_string.gsub(/;q=[\d.]+/, "").split(",").collect {|l| l.downcase.strip }.select{|l| l.present? } end private # Matches a browser name and version. # # @param agent [String] The User-Agent header. # @return [String|Symbol] The browser name or `:unknown`, if no match was found. def match_name_and_version(agent) catch(:name) do ::Brauser::Browser.browsers.each do |name, definitions| matched = match_definition(definitions[0], agent) if matched then @version = match_definition(definitions[1], name, agent) throw(:name, name) end end :unknown end end # Adjusts a browser version. def adjust_version(version) # Adjust version if version.blank? then version = "0.0" elsif version.is_a?(::MatchData) then version = version.to_a.last else version end end # Matches a browser platform. # # @param agent [String] The User-Agent header. # @return [String|Symbol] The browser platform or `:unknown`, if no match was found. def match_platform(agent) catch(:platform) do ::Brauser::Browser.platforms.each do |platform, definitions| matched = match_definition(definitions[0], @name, agent) throw(:platform, platform) if matched end :unknown end end # Matches a subject against a definition # # @param subject [Array] The subject to match. # @param definition [StringRegexp|Block] The definition. If a block, it will be yielded with the subject must return `true` if the subject was recognized. def match_definition(definition, *subject) if definition.is_a?(::Regexp) then definition.match(subject.last) elsif definition.respond_to?(:call) then definition.call(*subject) else subject.last == definition.ensure_string ? subject.last : nil end end end # Methods to query with chaining. module PartialQuerying # Checks if the browser is a specific name and optionally of a specific version and platform. # # @see #v? # @see #on? # # @param names [Symbol|Array] A list of specific names to match. Also, this meta-names are supported: `:capable` and `:tablet`. # @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`. # @param platforms [Symbol|Array] A list of specific platform to match. Valid values are all those possible for the platform attribute. # @return [Query] A query which can evaluated for concatenation or result. def is(names = [], versions = {}, platforms = []) self.parse_agent(@agent) if !@name names = adjust_names(names) versions = parse_versions_query(versions) platforms = platforms.ensure_array ::Brauser::Query.new(self, (names.blank? || (names.include?(@name) && check_capable(names))) && (versions.blank? || self.v?(versions)) && (platforms.blank? || self.on?(platforms)) ) end # Checks if the brower is a specific version. # # @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`. # @return [Query] A query which can evaluated for concatenation or result. def v(versions = {}) self.parse_agent(@agent) if !@version rv = true versions = if versions.is_a?(String) then parse_versions_query(versions) elsif !versions.is_a?(::Hash) then {} else versions end ::Brauser::Query.new(self, versions.all? { |operator, value| Brauser::Browser.compare_versions(@version, operator, value) }) end # Check if the browser is on a specific platform. # # @param platforms [Symbol|Array] A list of specific platform to match. # @return [Query] A query which can evaluated for concatenation or result. def on(platforms = []) self.parse_agent(@agent) if !@platform ::Brauser::Query.new(self, platforms.blank? || platforms.ensure_array.uniq.compact.collect {|p| p.ensure_string.to_sym }.include?(@platform)) end # Check if the browser accepts the specified languages. # # @param langs [String|Array] A list of languages to match against. # @return [Query] A query which can evaluated for concatenation or result. def accepts(langs = []) self.parse_accept_language(@accept_language) if !@languages ::Brauser::Query.new(self, (@languages & langs.ensure_array.uniq.compact.collect {|l| l.to_s }).present?) end private # Adjusts names for correct matching. # # @param names [Array] A list of names. # @return [Array] The adjusted list of names. def adjust_names(names) # Adjust names names = names.ensure_array.compact.collect {|n| n.ensure_string.to_sym } names << [:msie] if names.include?(:ie) names << [:chromium] if names.include?(:chrome) names << [:chrome, :firefox, :safari, :opera, :msie] if names.include?(:capable) names << [:ipad, :android, :kindle] if names.include?(:tablet) names.flatten.compact.uniq end # Checks if the browser is capable. # # @param names [Array] A list of names. # @return [Boolean] `true` if the browser is capable, `false` otherwise. def check_capable(names) !names.include?(:capable) || @name != :msie || Brauser::Browser.compare_versions(@version, :gte, 9) end # Parses a version query. # # @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`. # @return [Hash] The hash representation of the query. def parse_versions_query(versions) versions.is_a?(::Hash) ? versions : versions.ensure_string.split(/\s*&&\s*/).inject({}) do |prev, token| operator, version = parse_versions_query_component(token) prev[operator] = version if operator.present? && version.present? prev end end # Parses a token of a version query. # # @param token [String] The token to parse. # @return [Array] An operator and an argument. def parse_versions_query_component(token) operator, version = token.strip.split(/\s+/, 2).collect(&:strip) [{"<" => :lt, "<=" => :lte, "=" => :eq, "==" => :eq, ">" => :gt, ">=" => :gte}.fetch(operator, nil), version] end end # Methods to end querying. module Querying # Checks if the browser is a specific name and optionally of a specific version and platform. # # @see #v? # @see #on? # # @param names [Symbol|Array] A list of specific names to match. Also, this meta-names are supported: `:capable` and `:tablet`. # @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`. # @param platforms [Symbol|Array] A list of specific platform to match. Valid values are all those possible for the platform attribute. # @return [Boolean] `true` if current browser matches, `false` otherwise. def is?(names = [], versions = {}, platforms = []) self.is(names, versions, platforms).result end # Checks if the brower is a specific version. # # @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`. # @return [Boolean] `true` if current browser matches, `false` otherwise. def v?(versions = {}) self.v(versions).result end # Check if the browser is on a specific platform. # # @param platforms [Symbol|Array] A list of specific platform to match. # @return [Boolean] `true` if current browser matches, `false` otherwise. def on?(platforms = []) self.on(platforms).result end # Check if the browser accepts the specified languages. # # @param langs [String|Array] A list of languages to match against. # @return [Boolean] `true` if current browser matches, `false` otherwise. def accepts?(langs = []) self.accepts(langs).result end end end # This class represents a detection of the current user browser. # # @attribute agent # @return [String] The raw User-Agent HTTP header. # @attribute accept_language # @return [String] The raw Accept-Language HTTP header. # @attribute languages # @return [Array] The accepted languages. # @attribute name # @return [String] The current browser name. # @attribute version # @return [String] The current browser version. # @attribute platform # @return [String] The current browser platform. class Browser attr_accessor :agent attr_accessor :accept_language attr_accessor :languages attr_accessor :name attr_accessor :version attr_accessor :platform # Aliases alias :ua :agent alias :ua= :agent= include ::Brauser::BrowserMethods::General include ::Brauser::BrowserMethods::Attributes include ::Brauser::BrowserMethods::Register include ::Brauser::BrowserMethods::Parsing include ::Brauser::BrowserMethods::PartialQuerying include ::Brauser::BrowserMethods::Querying # Creates a new browser. # # @param agent [String] The User-Agent HTTP header. # @param accept_language [String] The Accept-Language HTTP header. def initialize(agent = "", accept_language = "") ::Brauser::Browser.register_default_browsers ::Brauser::Browser.register_default_platforms ::Brauser::Browser.register_default_languages @agent = agent @accept_language = accept_language @languages = self.parse_accept_language(@accept_language) if @accept_language self.parse_agent(@agent) if @agent end # This method enables the use of dynamic queries in just one method. # # For example: # # ```ruby # browser.is_msie_gt_4_1__on_windows? # #=> true # ``` # # If you don't provide a trailing `?`, you will get a Brauser::Query. # # If the syntax is invalid, a `NoMethodError` exception will be raised. # # @param query [String] The query to issue. Use `__` to separate query and `_` in place of `.` in the version. # @param arguments [Array] The arguments to pass the method. Unused from the query. # @param block [Proc] A block to pass to the method. Unused from the query. # @return [Boolean|Query|nil] A query or a boolean value (if `method` ends with `?`). If the query is not valid, `NoMethodError` will be raised. def method_missing(query, *arguments, &block) begin parsed_query = parse_query(query.ensure_string) rv = execute_query(parsed_query) || Brauser::Query.new(self, false) query.ensure_string =~ /\?$/ ? rv.result : rv rescue NoMethodError super(query, *arguments, &block) end end # Returns the current browser as a string. # # @see #classes # # @return [String] A string representation of the current browser. def to_s self.classes end private # Parse query, getting all arguments. # @param query [String] The query to issue. Use `__` to separate query and `_` in place of `.` in the version. # @return [Array] And array of `[method, arguments]` entries. def parse_query(query) query.gsub(/\?$/, "").split("__").collect do |part| parse_query_part(part) end end # Handles a part of a query. # # @param part [String] A part of a query. # @return [Boolean|Query|nil] A query or a boolean value (if `method` ends with `?`). If the query is not valid, `NoMethodError` will be raised. def parse_query_part(part) method, arguments = part.split("_", 2) if method == "v" then arguments = parse_query_version(arguments) elsif !["is", "on"].include?(method) raise NoMethodError end [method, arguments] end # Parses the version for a query. # # @param version [String] The version to parse. # @return [String] The parsed version. def parse_query_version(version) [ [/_?eq_?/, " == "], # Parse == [/_?lte_?/, " <= "], # Parse <= [/_?gte_?/, " >= "], # Parse >= [/_?lt_?/, " < "], # Parse < [/_?gt_?/, " > "], # Parse > [/_?and_?/, " && "], # Parse && ["_", "."], # Dot notation [/\s+/, " "] ].inject(version) { |current, parse| current.gsub(parse[0], parse[1])}.strip end # Executes a parsed query # # @param query [Array] And array of `[method, arguments]` entries. # @return [Brauser::Query] The result of the query. def execute_query(query) query.inject(Brauser::Query.new(self, true)) { |rv, call| break if !rv.result rv.send(call[0], *call[1]) } end end end