browscapper 0.0.1

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/.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