browscapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ *.orig
2
+ .*.swp
3
+ .*.swo
4
+ *.tmp
5
+ *.patch
6
+ *.kpf
7
+ *~
8
+ .DS_Store
9
+ Thumbs.db
10
+ pkg/
11
+ doc/
12
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 J Smith <dark.panda@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,43 @@
1
+
2
+ = Browscapper
3
+
4
+ I suppose this could also be called Yet Another Browscap.ini Reader.
5
+
6
+ Browscapper.load('path/to/browscap.ini')
7
+ Browscapper.match('Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8')
8
+ #=> Browscapper::UserAgent
9
+
10
+ Browscapper.matches
11
+ #=> Array of Browscapper::UserAgents, in order of best-to-worst matches.
12
+
13
+ == File Formats
14
+
15
+ The most widely used browscap.ini file is probably Gary Keith's, found at
16
+ https://browsers.garykeith.com/downloads . For the sake of convenience and
17
+ compatibiilty, Browscapper.load will look in the current directory for
18
+ a browscap.ini file. However, Browscapper can also read in browscap.csv
19
+ files as well, which are also available from Gary's site, as well as YAML
20
+ files and files written in Ruby's own Marshal'd file format. In fact, for the
21
+ sake of performance and speed, you may want to consider dumping browscap.ini
22
+ files into the Marshal'd format, which loads much faster than any of the
23
+ other file formats. For the sake of comparison, here are some benchmarks from
24
+ on my current development laptop (MacBook Pro 10,1 with a solid state drive):
25
+
26
+ bench_csv 1.111839s
27
+ bench_ini 3.801561s
28
+ bench_marshal 0.081942s
29
+
30
+ These are just the averages from a few runs, but the result is pretty clear
31
+ all the same: the Marshal'd format is by far quicker. (The YAML parser
32
+ currently has some problems with some messed up encodings and isn't
33
+ really recommended for prime time.)
34
+
35
+ To dump Browscapper into a Marshal'd format, you can either call
36
+ Browscapper.dump and write the results to a file or you can use the provided
37
+ rake task thusly:
38
+
39
+ rake browscapper:dump IN=browscap.ini OUT=browscap.dump
40
+
41
+ == License
42
+
43
+ See the MIT-LICENSE file for details.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+
5
+ gem 'rdoc', '~> 3.12'
6
+
7
+ require 'rubygems/package_task'
8
+ require 'rake/testtask'
9
+ require 'rdoc/task'
10
+ require 'bundler/gem_tasks'
11
+
12
+ if RUBY_VERSION >= '1.9'
13
+ begin
14
+ gem 'psych'
15
+ rescue Exception => e
16
+ # it's okay, fall back on the bundled psych
17
+ end
18
+ end
19
+
20
+ $:.push 'lib'
21
+
22
+ version = Browscapper::VERSION
23
+
24
+ desc 'Test browscapper library'
25
+ Rake::TestTask.new(:test) do |t|
26
+ t.test_files = FileList['test/**/*_tests.rb']
27
+ t.verbose = !!ENV['VERBOSE_TESTS']
28
+ t.warning = !!ENV['WARNINGS']
29
+ end
30
+
31
+ desc 'Build docs'
32
+ Rake::RDocTask.new do |t|
33
+ t.title = "browscapper #{version}"
34
+ t.main = 'README.rdoc'
35
+ t.rdoc_dir = 'doc'
36
+ t.rdoc_files.include('README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb')
37
+ end
38
+
39
+ desc "Converts a browscap file to another format"
40
+ task "browscapper:dump" do
41
+ require File.join(File.dirname(__FILE__), %w{ lib browscapper })
42
+
43
+ if !ENV['IN'] || !ENV['OUT']
44
+ puts "Usage: IN=browscap OUT=browscap.dump"
45
+ puts
46
+ puts "Converts a browscap file to a ruby Marshal'd format. These files"
47
+ puts "are much quicker to load than CSV or INI files."
48
+ exit
49
+ end
50
+
51
+ Browscapper.load(ENV['IN'])
52
+ File.open(ENV['OUT'], 'wb') do |fp|
53
+ fp.write(Marshal.dump(Browscapper.entries))
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/browscapper/version', __FILE__)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = %q{browscapper}
7
+ s.version = Browscapper::VERSION
8
+
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.authors = ["J Smith"]
11
+ s.description = "A browscap file parser and matcher."
12
+ s.summary = s.description
13
+ s.email = "dark.panda@gmail.com"
14
+ s.extra_rdoc_files = [
15
+ "README.rdoc"
16
+ ]
17
+ s.files = `git ls-files`.split($\)
18
+ s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
19
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
20
+ s.homepage = "http://github.com/dark-panda/browscapper"
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_dependency("rdoc")
24
+ s.add_dependency("rake", ["~> 0.9"])
25
+ s.add_dependency("minitest")
26
+ s.add_dependency("inifile")
27
+ end
28
+
@@ -0,0 +1,63 @@
1
+ # encoding: BINARY
2
+
3
+ if RUBY_VERSION.to_f < 1.9
4
+ require 'fastercsv'
5
+ else
6
+ require 'csv'
7
+ end
8
+
9
+ module Browscapper
10
+ module CSVReader
11
+ CSV_ENGINE = if defined?(FasterCSV)
12
+ FasterCSV
13
+ else
14
+ CSV
15
+ end
16
+
17
+ class << self
18
+ include Reader
19
+
20
+ def load(file)
21
+ csv = CSV_ENGINE.open(file, 'rb')
22
+
23
+ # skip header
24
+ 2.times { csv.shift }
25
+ headers = csv.shift
26
+
27
+ entries = Hash.new
28
+
29
+ csv.each do |l|
30
+ entry = UserAgent.new
31
+ headers.each_with_index do |v, i|
32
+ entry[v] = case l[i]
33
+ when 'false'
34
+ false
35
+ when 'true'
36
+ true
37
+ when /^\d+$/
38
+ l[i].to_i
39
+ else
40
+ l[i]
41
+ end
42
+ end
43
+
44
+ entry.user_agent = if entry.user_agent
45
+ entry.user_agent.sub!(/^\[(.+)\]$/, '\1')
46
+ elsif entry.browser
47
+ entry.browser
48
+ end
49
+
50
+ entry.pattern = pattern_to_regexp(entry.user_agent.to_s)
51
+
52
+ if entries[entry.parent]
53
+ entry.merge!(entries[entry.parent])
54
+ end
55
+
56
+ entries[entry.user_agent] = entry
57
+ end
58
+
59
+ entries
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: BINARY
2
+
3
+ require 'inifile'
4
+
5
+ module Browscapper
6
+ module INIReader
7
+ class << self
8
+ include Reader
9
+
10
+ def load(file)
11
+ ini = IniFile.load(file, :encoding => 'BINARY')
12
+ ini.delete_section('GJK_Browscap_Version')
13
+
14
+ injector = proc { |memo, section|
15
+ unless memo[section]
16
+ entry = UserAgent.new
17
+ ini[section].each do |k, v|
18
+ entry[k] = case v
19
+ when 'false'
20
+ false
21
+ when 'true'
22
+ true
23
+ when /^\d+$/
24
+ v.to_i
25
+ else
26
+ v
27
+ end
28
+ end
29
+ entry.user_agent = section
30
+ entry.pattern = pattern_to_regexp(entry.user_agent)
31
+
32
+ memo[section] = entry
33
+
34
+ if entry.parent
35
+ memo = injector.call(memo, entry.parent)
36
+ entry.merge!(memo[entry.parent])
37
+ end
38
+ end
39
+
40
+ memo
41
+ }
42
+
43
+ ini.sections.inject({}, &injector)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: BINARY
2
+
3
+ module Browscapper
4
+ module MarshalReader
5
+ class << self
6
+ include Reader
7
+
8
+ def load(file)
9
+ Marshal.load(File.open(file, 'rb').read)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: BINARY
2
+
3
+ require 'yaml'
4
+
5
+ module Browscapper
6
+ module YAMLReader
7
+ class << self
8
+ def load(file)
9
+ YAML.load(File.open(file, 'rb'))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: BINARY
2
+
3
+ module Browscapper
4
+ module Reader
5
+ def pattern_to_regexp(pattern)
6
+ pattern = pattern.dup
7
+
8
+ if pattern.respond_to?(:force_encoding)
9
+ pattern.force_encoding('BINARY')
10
+ end
11
+
12
+ pattern.downcase!
13
+ pattern.gsub!(/([\^\$\(\)\[\]\.\-])/, "\\\\\\1")
14
+ pattern.gsub!('?', '.')
15
+ pattern.gsub!('*', '.*?')
16
+
17
+ Regexp.new("^#{pattern}$")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: BINARY
2
+
3
+ module Browscapper
4
+ class UserAgent
5
+ include Enumerable
6
+
7
+ ACCESSORS = [
8
+ :parent, :user_agent, :comment, :browser, :version, :major_version,
9
+ :minor_version, :platform, :platform_version, :alpha, :beta, :win16, :win32, :win64,
10
+ :frames, :iframes, :tables, :cookies, :background_sounds, :vbscript,
11
+ :javascript, :java_applets, :activex_controls, :banned, :mobile_device,
12
+ :syndication_reader, :crawler, :css_version, :aol_version, :user_agent_id,
13
+ :css, :cdf, :aol, :device_name, :device_maker, :rendering_engine_name,
14
+ :rendering_engine_version, :rendering_engine_description
15
+ ]
16
+
17
+ ACCESSORS.each do |accessor|
18
+ self.class_eval do
19
+ attr_accessor accessor
20
+ end
21
+ end
22
+
23
+ attr_accessor :pattern
24
+
25
+ MAP = {
26
+ 'useragent' => :user_agent,
27
+ 'version' => :version,
28
+ 'majorver' => :major_version,
29
+ 'minorver' => :minor_version,
30
+ 'majorversion' => :major_version,
31
+ 'minorversion' => :minor_version,
32
+ 'backgroundsounds' => :background_sounds,
33
+ 'javaapplets' => :java_applets,
34
+ 'activexcontrols' => :activex_controls,
35
+ 'isbanned' => :banned,
36
+ 'ismobiledevice' => :mobile_device,
37
+ 'mobiledevice' => :mobile_device,
38
+ 'issyndicationreader' => :syndication_reader,
39
+ 'syndicationreader' => :syndication_reader,
40
+ 'cssversion' => :css_version,
41
+ 'aolversion' => :aol_version,
42
+ 'useragentid' => :user_agent_id,
43
+ 'supportscss' => :css,
44
+ 'renderingengine_name' => :rendering_engine_name,
45
+ 'renderingengine_version' => :rendering_engine_version,
46
+ 'renderingengine_description' => :rendering_engine_description
47
+ }
48
+
49
+ def initialize(hash = {})
50
+ self.merge!(hash)
51
+ end
52
+
53
+ def each
54
+ self.to_hash.each do |k, v|
55
+ yield k, v
56
+ end
57
+ end
58
+
59
+ def to_hash
60
+ @to_hash ||= ACCESSORS.inject({}) do |memo, accessor|
61
+ memo[accessor] = self.send(accessor)
62
+ memo
63
+ end
64
+ end
65
+
66
+ def []=(key, value)
67
+ key = key.to_s.downcase
68
+ key = "#{MAP[key] || key}="
69
+ self.send(key, value) if self.respond_to?(key)
70
+ end
71
+
72
+ def [](key)
73
+ key = key.to_s.downcase
74
+ key = MAP[key] || key
75
+ self.send(key) if self.respond_to?(key)
76
+ end
77
+
78
+ def merge!(parent)
79
+ parent = parent.to_hash if parent.is_a?(UserAgent)
80
+ ACCESSORS.each do |k, v|
81
+ if parent.has_key?(k)
82
+ val = self.send(k)
83
+ if val == 'default' || val.nil?
84
+ self.send("#{k}=", parent[k])
85
+ end
86
+ end
87
+ end
88
+ self
89
+ end
90
+
91
+ def ==(other)
92
+ ACCESSORS.each do |accessor|
93
+ return false if self.send(accessor) != other.send(accessor)
94
+ end
95
+ true
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: BINARY
2
+
3
+ module Browscapper
4
+ VERSION = "0.0.1"
5
+ end
6
+
@@ -0,0 +1,71 @@
1
+ # encoding: BINARY
2
+
3
+ module Browscapper
4
+ autoload :UserAgent, 'browscapper/user_agent'
5
+ autoload :Reader, 'browscapper/reader'
6
+ autoload :CSVReader, 'browscapper/reader/csv_reader'
7
+ autoload :YAMLReader, 'browscapper/reader/yaml_reader'
8
+ autoload :INIReader, 'browscapper/reader/ini_reader'
9
+ autoload :MarshalReader, 'browscapper/reader/marshal_reader'
10
+
11
+ class << self
12
+ attr_reader :entries, :file
13
+ MATCH_CACHE = Hash.new
14
+
15
+ def load(file = File.join(%w{ . browscap.ini }))
16
+ if !File.exists?(file)
17
+ raise ArgumentError.new("File #{file} not found.")
18
+ end
19
+
20
+ reader = case file.downcase
21
+ when /\.csv$/
22
+ CSVReader
23
+ when /\.ya?ml$/
24
+ YAMLReader
25
+ when /\.ini$/
26
+ INIReader
27
+ else
28
+ MarshalReader
29
+ end
30
+
31
+ @file = file
32
+ @entries = reader.load(file)
33
+ self
34
+ end
35
+
36
+ def dump(format = :marshal)
37
+ @entries or Browscapper.load
38
+
39
+ Marshal.dump(@entries)
40
+ end
41
+
42
+ def clear_cache
43
+ MATCH_CACHE.clear
44
+ end
45
+
46
+ def matches(ua)
47
+ @entries or self.load
48
+
49
+ ua_str = ua.downcase
50
+ ua_len = ua.length
51
+
52
+ MATCH_CACHE[ua] ||= @entries.select { |k, v|
53
+ v[:pattern] =~ ua_str if v
54
+ }.sort_by { |(k, v)|
55
+ ua_len - v[:user_agent].gsub(/[?*]/, '').length
56
+ }.collect { |(k, v)|
57
+ v
58
+ }
59
+ end
60
+
61
+ def match(ua)
62
+ if MATCH_CACHE[ua] && !MATCH_CACHE[ua].empty?
63
+ MATCH_CACHE[ua].first
64
+ else
65
+ m = self.matches(ua)
66
+ m.first if !m.empty?
67
+ end
68
+ end
69
+ alias :query :match
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: BINARY
2
+
3
+ $: << './lib'
4
+
5
+ require 'rubygems'
6
+ require 'minitest/autorun'
7
+ require './lib/browscapper'
8
+
9
+ class BrowscapperTest < MiniTest::Unit::TestCase
10
+ FIREFOX_ON_OSX = Browscapper::UserAgent.new({
11
+ :activex_controls => false,
12
+ :alpha => false,
13
+ :aol_version => 0,
14
+ :background_sounds => false,
15
+ :beta => false,
16
+ :browser => "Firefox",
17
+ :comment => "Firefox 3.6",
18
+ :cookies => true,
19
+ :crawler => false,
20
+ :css_version => 3,
21
+ :device_name => "unknown",
22
+ :device_maker => "unknown",
23
+ :frames => true,
24
+ :iframes => true,
25
+ :java_applets => true,
26
+ :javascript => true,
27
+ :major_version => 3,
28
+ :minor_version => 6,
29
+ :mobile_device => false,
30
+ :parent => "Firefox 3.6",
31
+ :platform => "MacOSX",
32
+ :platform_version => "unknown",
33
+ :rendering_engine_name => "Gecko",
34
+ :rendering_engine_version => "unknown",
35
+ :rendering_engine_description => "For Firefox, Camino, K-Meleon, SeaMonkey, Netscape, and other Gecko-based browsers.",
36
+ :syndication_reader => false,
37
+ :tables => true,
38
+ :user_agent => "Mozilla/5.0 (Macintosh; *; *Mac OS X*; *; rv:1.9.2*) Gecko/* Firefox/3.6*",
39
+ :user_agent_id => nil,
40
+ :vbscript => false,
41
+ :version => "3.6",
42
+ :win16 => false,
43
+ :win32 => false,
44
+ :win64 => false,
45
+ })
46
+
47
+ FIREFOX_ON_OSX.pattern = /^mozilla\/5\.0 \(macintosh; .*; .*mac os x.*; .*; rv:1\.9\.2.*\) gecko\/.* firefox\/3\.6.*$/
48
+
49
+ def test_browscap_ini
50
+ Browscapper.load(File.join('vendor', 'browscap.dump'))
51
+ match = Browscapper.match('Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8')
52
+
53
+ FIREFOX_ON_OSX.each do |k, v|
54
+ assert(v == match[k], " Expected #{v.inspect} for #{k}, got #{match[k].inspect}")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,37 @@
1
+
2
+ # encoding: BINARY
3
+
4
+ $: << './lib'
5
+
6
+ require 'rubygems'
7
+ require 'minitest/autorun'
8
+ require 'minitest/benchmark' if ENV['BENCH']
9
+ require './lib/browscapper'
10
+ require 'benchmark'
11
+
12
+ class ReaderTests < MiniTest::Unit::TestCase
13
+ def bench_csv
14
+ assert_performance_constant do |n|
15
+ Browscapper.load('vendor/browscap.csv')
16
+ end
17
+ end
18
+
19
+ def bench_yaml
20
+ assert_performance_constant do |n|
21
+ Browscapper.load('vendor/browscap.yml')
22
+ end
23
+ end
24
+
25
+ def bench_ini
26
+ assert_performance_constant do |n|
27
+ Browscapper.load('vendor/browscap.ini')
28
+ end
29
+ end
30
+
31
+ def bench_marshal
32
+ assert_performance_constant do |n|
33
+ Browscapper.load('vendor/browscap.dump')
34
+ end
35
+ end
36
+ end
37
+
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: browscapper
3
+ version: !ruby/object:Gem::Version
4
+ version: !binary |-
5
+ MC4wLjE=
6
+ prerelease:
7
+ platform: ruby
8
+ authors:
9
+ - J Smith
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-09-29 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rdoc
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rake
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: '0.9'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '0.9'
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: inifile
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ description: A browscap file parser and matcher.
80
+ email: dark.panda@gmail.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files:
84
+ - README.rdoc
85
+ files:
86
+ - .gitignore
87
+ - Gemfile
88
+ - MIT-LICENSE
89
+ - README.rdoc
90
+ - Rakefile
91
+ - browscapper.gemspec
92
+ - lib/browscapper.rb
93
+ - lib/browscapper/reader.rb
94
+ - lib/browscapper/reader/csv_reader.rb
95
+ - lib/browscapper/reader/ini_reader.rb
96
+ - lib/browscapper/reader/marshal_reader.rb
97
+ - lib/browscapper/reader/yaml_reader.rb
98
+ - lib/browscapper/user_agent.rb
99
+ - lib/browscapper/version.rb
100
+ - test/browscapper_tests.rb
101
+ - test/reader_tests.rb
102
+ homepage: http://github.com/dark-panda/browscapper
103
+ licenses: []
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 1.8.24
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: A browscap file parser and matcher.
126
+ test_files:
127
+ - test/browscapper_tests.rb
128
+ - test/reader_tests.rb