postrank-uri 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # PostRank URI
2
+
3
+ A collection of convenience methods (Ruby 1.8 & Ruby 1.9) for dealing with extracting, (un)escaping, normalization, and canonicalization of URIs. At PostRank we process over 20M URI associated activities each day, and we need to make sure that we can reliably extract the URIs from a variety of text formats, deal with all the numerous and creative ways users like to escape and unescape their URIs, normalize the resulting URIs, and finally apply a set of custom canonicalization rules to make sure that we can cross-reference when the users are talking about the same URL.
4
+
5
+ In a nutshell, we need to make sure that creative cases like the ones below all resolve to same URI:
6
+
7
+ - http://igvita.com/
8
+ - http://igvita.com///
9
+ - http://igvita.com/../?#
10
+ - http://igvita.com/a/../?
11
+ - http://igvita.com/a/../?utm_source%3Danalytics
12
+ - ... and the list goes on - check the specs.
13
+
14
+ ## API
15
+
16
+ - **PostRank::URI.extract(text)** - Detect URIs in text, discard bad TLD's
17
+ - **PostRank::URI.clean(uri)** - Unescape, normalize, apply c18n filters - 95% use case.
18
+
19
+ - **PostRank::URI.normalize(uri)** - Apply RFC normalization rules, discard extra path characters, drop anchors
20
+ - **PostRank::URI.unescape(uri)** - Unescape URI entities, handle +/%20's, etc
21
+ - **PostRank::URI.escape(uri)** - Escape URI
22
+
23
+ ## Example
24
+
25
+ >> PostRank::URI.extract('some random text with http://link.to somecanadiansite.ca')
26
+ [
27
+ [0] "http://link.to/",
28
+ [1] "http://somecanadiansite.ca/"
29
+ ]
30
+
31
+ >> PostRank::URI.clean('link.to?a=b&utm_source=FeedBurner#stuff')
32
+ [
33
+ [0] "http://link.to/?a=b"
34
+ ]
35
+
36
+ ## C18N
37
+
38
+ As part of URI canonicalization the library will remove common tracking parameters from Google Analytics and several other providers. Beyond that, host-specific rules are also applied. For example, nytimes.com likes to add a 'partner' query parameter for tracking purposes, but which has no effect on the content - hence, it is removed from the URI. For full list, see the c18n.yml file.
39
+
40
+ Detecting "duplicate URLs" is a hard problem to solve (expensive in all senses), instead we are compiling a manually assembled database. If you find cases which are missing, please do report them, or send us a pull request!
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec'
5
+ require 'rspec/core/rake_task'
6
+
7
+ Rspec::Core::RakeTask.new do |t|
8
+ t.rspec_opts = '--color'
9
+ end
@@ -0,0 +1,126 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'addressable/uri'
4
+ require 'domainatrix'
5
+ require 'yaml'
6
+
7
+ module PostRank
8
+ module URI
9
+
10
+ c18ndb = YAML.load_file(File.dirname(__FILE__) + '/postrank-uri/c18n.yml')
11
+
12
+ C18N = {}
13
+ C18N[:global] = c18ndb[:all].freeze
14
+ C18N[:hosts] = c18ndb[:hosts].inject({}) {|h,(k,v)| h[/#{Regexp.escape(k)}$/.freeze] = v; h}
15
+
16
+ URIREGEX = {}
17
+ URIREGEX[:protocol] = /https?:\/\//i
18
+ URIREGEX[:valid_preceding_chars] = /(?:|\.|[^-\/"':!=A-Z0-9_@@]|^|\:)/i
19
+ URIREGEX[:valid_domain] = /(?:[^[:punct:]\s][\.-](?=[^[:punct:]\s])|[^[:punct:]\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i
20
+ URIREGEX[:valid_general_url_path_chars] = /[a-z0-9!\*';:=\+\,\$\/%#\[\]\-_~]/i
21
+
22
+ # Allow URL paths to contain balanced parens
23
+ # 1. Used in Wikipedia URLs like /Primer_(film)
24
+ # 2. Used in IIS sessions like /S(dfd346)/
25
+ URIREGEX[:wikipedia_disambiguation] = /(?:\(#{URIREGEX[:valid_general_url_path_chars]}+\))/i
26
+
27
+ # Allow @ in a url, but only in the middle. Catch things like http://example.com/@user
28
+ URIREGEX[:valid_url_path_chars] = /(?:
29
+ #{URIREGEX[:wikipedia_disambiguation]}|
30
+ @#{URIREGEX[:valid_general_url_path_chars]}+\/|
31
+ [\.,]#{URIREGEX[:valid_general_url_path_chars]}+|
32
+ #{URIREGEX[:valid_general_url_path_chars]}+
33
+ )/ix
34
+
35
+ # Valid end-of-path chracters (so /foo. does not gobble the period).
36
+ # 1. Allow =&# for empty URL parameters and other URL-join artifacts
37
+ URIREGEX[:valid_url_path_ending_chars] = /[a-z0-9=_#\/\+\-]|#{URIREGEX[:wikipedia_disambiguation]}/io
38
+ URIREGEX[:valid_url_query_chars] = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i
39
+ URIREGEX[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/]/i
40
+
41
+ URIREGEX[:valid_url] = %r{
42
+ ( # $1 total match
43
+ (#{URIREGEX[:valid_preceding_chars]}) # $2 Preceeding chracter
44
+ ( # $3 URL
45
+ (https?:\/\/)? # $4 Protocol
46
+ (#{URIREGEX[:valid_domain]}) # $5 Domain(s) and optional post number
47
+ (/
48
+ (?:
49
+ # 1+ path chars and a valid last char
50
+ #{URIREGEX[:valid_url_path_chars]}+#{URIREGEX[:valid_url_path_ending_chars]}|
51
+ # Optional last char to handle /@foo/ case
52
+ #{URIREGEX[:valid_url_path_chars]}+#{URIREGEX[:valid_url_path_ending_chars]}?|
53
+ # Just a # case
54
+ #{URIREGEX[:valid_url_path_ending_chars]}
55
+ )?
56
+ )? # $6 URL Path and anchor
57
+ # $7 Query String
58
+ (\?#{URIREGEX[:valid_url_query_chars]}*#{URIREGEX[:valid_url_query_ending_chars]})?
59
+ )
60
+ )
61
+ }iox;
62
+
63
+ URIREGEX[:escape] = /([^ a-zA-Z0-9_.-]+)/x
64
+ URIREGEX[:unescape] = /((?:%[0-9a-fA-F]{2})+)/x
65
+ URIREGEX.each_pair{|k,v| v.freeze }
66
+
67
+ def self.extract(text)
68
+ return [] if !text
69
+ urls = []
70
+ text.to_s.scan(URIREGEX[:valid_url]) do |all, before, url, protocol, domain, path, query|
71
+ begin
72
+ url = clean(url).to_s
73
+ Domainatrix.parse(url)
74
+ urls.push url
75
+ rescue NoMethodError
76
+ end
77
+ end
78
+
79
+ urls.compact
80
+ end
81
+
82
+ def self.escape(uri)
83
+ uri.gsub(URIREGEX[:escape]) do
84
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
85
+ end.gsub(' ','%20')
86
+ end
87
+
88
+ def self.unescape(uri)
89
+ uri.tr('+', ' ').gsub(URIREGEX[:unescape]) do
90
+ [$1.delete('%')].pack('H*')
91
+ end
92
+ end
93
+
94
+ def self.clean(uri)
95
+ normalize(c18n(unescape(uri))).to_s
96
+ end
97
+
98
+ def self.normalize(uri)
99
+ u = parse(uri)
100
+ u.path = u.path.squeeze('/')
101
+ u.query = nil if u.query && u.query.empty?
102
+ u.fragment = nil
103
+ u
104
+ end
105
+
106
+ def self.c18n(uri)
107
+ u = parse(uri)
108
+
109
+ if q = u.query_values(:notation => :flat_array)
110
+ q.delete_if { |k,v| C18N[:global].include?(k) }
111
+ q.delete_if { |k,v| C18N[:hosts].find {|r,p| u.host =~ r && p.include?(k) } }
112
+ end
113
+
114
+ u.query_values = q
115
+ u
116
+ end
117
+
118
+ def self.parse(uri)
119
+ return uri if uri.is_a? Addressable::URI
120
+
121
+ uri = uri.index(URIREGEX[:protocol]) == 0 ? uri : "http://#{uri}"
122
+ Addressable::URI.parse(uri).normalize
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,37 @@
1
+ ---
2
+ :all:
3
+ - utm_source # Google Analytics: campaign source
4
+ - utm_medium # Google Analytics: campaign medium
5
+ - utm_term # Google Anlaytics: campaign term
6
+ - utm_content # Google Analytics: campaign content
7
+ - utm_campaign # Google Analytics: campaign name
8
+ - sms_ss # addthis.com tracker
9
+ - awesm # awe.sm tracker
10
+
11
+ :hosts:
12
+ nytimes.com:
13
+ - partner
14
+ - emc
15
+ - _r
16
+ washingtonpost.com:
17
+ - nav
18
+ - wprss
19
+ cnn.com:
20
+ - eref
21
+ latimes.com:
22
+ - track
23
+ usatoday.com:
24
+ - csp
25
+ economist.com:
26
+ - fsrc
27
+ espn.go.com:
28
+ - campaign
29
+ - source
30
+ dw-world.de:
31
+ - maca
32
+ repubblica.it:
33
+ - rss
34
+ welt.de:
35
+ - wtmc
36
+ usatoday.com:
37
+ - csp
@@ -0,0 +1,5 @@
1
+ module PostRank
2
+ module URI
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "postrank-uri/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "postrank-uri"
7
+ s.version = PostRank::URI::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Ilya Grigorik"]
10
+ s.email = ["ilya@igvita.com"]
11
+ s.homepage = "http://rubygems.org/gems/postrank-uri"
12
+ s.summary = "URI normalization, c18n, escaping, and extraction"
13
+ s.description = s.summary
14
+
15
+ s.rubyforge_project = "postrank-uri"
16
+
17
+ s.add_dependency "addressable"
18
+ s.add_dependency "domainatrix"
19
+ s.add_development_dependency "rspec"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,39 @@
1
+ ---
2
+ - - http://www.nytimes.com/2010/12/16/world/europe/16russia.html?_r=1&partner=rss&emc=rss
3
+ - http://www.nytimes.com/2010/12/16/world/europe/16russia.html
4
+
5
+ - - http://dotearth.blogs.nytimes.com/2010/12/14/beyond-political-science/?partner=rss&emc=rss
6
+ - http://dotearth.blogs.nytimes.com/2010/12/14/beyond-political-science/
7
+
8
+ - - http://www.washingtonpost.com/wp-dyn/content/article/2010/12/14/AR2010121406045.html?nav=rss_email/components
9
+ - http://www.washingtonpost.com/wp-dyn/content/article/2010/12/14/AR2010121406045.html
10
+
11
+ - - http://www.washingtonpost.com/wp-dyn/content/article/2010/12/14/AR2010121407704.html?wprss=rss_politics
12
+ - http://www.washingtonpost.com/wp-dyn/content/article/2010/12/14/AR2010121407704.html
13
+
14
+ - - http://edition.cnn.com/2010/US/12/14/afghanistan.review/index.html?eref=edition
15
+ - http://edition.cnn.com/2010/US/12/14/afghanistan.review/index.html
16
+
17
+ - - http://www.latimes.com/news/politics/la-na-steele-rnc-20101214,0,6423667.story?track=rss&utm_source=feedburner
18
+ - http://www.latimes.com/news/politics/la-na-steele-rnc-20101214,0,6423667.story
19
+
20
+ - - http://www.usatoday.com/sports/baseball/2010-12-14-reggie-jackson-yankees-baby-boomers_N.htm?csp=34sports
21
+ - http://www.usatoday.com/sports/baseball/2010-12-14-reggie-jackson-yankees-baby-boomers_N.htm
22
+
23
+ - - http://www.economist.com/node/17522368?story_id=17522368&fsrc=rss
24
+ - http://www.economist.com/node/17522368?story_id=17522368
25
+
26
+ - - http://sports.espn.go.com/dallas/mlb/news/story?id=5919388&campaign=rss&source=MLBHeadlines
27
+ - http://sports.espn.go.com/dallas/mlb/news/story?id=5919388
28
+
29
+ - - http://www.dw-world.de/dw/article/0,,6330472,00.html?maca=en-rss-en-all-1573-rdf
30
+ - http://www.dw-world.de/dw/article/0,,6330472,00.html
31
+
32
+ - - http://www.repubblica.it/rubriche/il-caso-del-giorno/2010/12/13/news/riscossa_aeffe-10153565/?rss
33
+ - http://www.repubblica.it/rubriche/il-caso-del-giorno/2010/12/13/news/riscossa_aeffe-10153565/
34
+
35
+ - - http://www.welt.de/sport/Der-Hoellenritt-des-Fussball-Profis-Jean-Marc-Bosman.html?wtmc=RSS.Sport.Fussball
36
+ - http://www.welt.de/sport/Der-Hoellenritt-des-Fussball-Profis-Jean-Marc-Bosman.html
37
+
38
+ - - http://www.usatoday.com/life/television/news/2011-01-19-race19_ST_N.htm?csp=34life
39
+ - http://www.usatoday.com/life/television/news/2011-01-19-race19_ST_N.htm
data/spec/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+
4
+ require 'lib/postrank-uri'
@@ -0,0 +1,186 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'helper'
4
+
5
+ describe PostRank::URI do
6
+
7
+ let(:igvita) { 'http://igvita.com/' }
8
+
9
+ context "escaping" do
10
+ it "should escape PostRank::URI string" do
11
+ PostRank::URI.escape('id=1').should == 'id%3D1'
12
+ end
13
+
14
+ it "should escape spaces as %20's" do
15
+ PostRank::URI.escape('id= 1').should match('%20')
16
+ end
17
+ end
18
+
19
+ context "unescape" do
20
+ it "should unescape PostRank::URI" do
21
+ PostRank::URI.unescape(PostRank::URI.escape('id=1')).should == 'id=1'
22
+ end
23
+
24
+ it "should unescape PostRank::URI with spaces" do
25
+ PostRank::URI.unescape(PostRank::URI.escape('id= 1')).should == 'id= 1'
26
+ end
27
+
28
+ context "accept improperly escaped PostRank::URI strings" do
29
+ # See http://tools.ietf.org/html/rfc3986#section-2.3
30
+
31
+ it "should unescape PostRank::URI with spaces encoded as '+'" do
32
+ PostRank::URI.unescape('id=+1').should == 'id= 1'
33
+ end
34
+
35
+ it "should unescape PostRank::URI with spaces encoded as '+'" do
36
+ PostRank::URI.unescape('id%3D+1').should == 'id= 1'
37
+ end
38
+
39
+ it "should unescape PostRank::URI with spaces encoded as %20" do
40
+ PostRank::URI.unescape('id=%201').should == 'id= 1'
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ context "normalize" do
47
+ def n(uri)
48
+ PostRank::URI.normalize(uri).to_s
49
+ end
50
+
51
+ it "should normalize paths in PostRank::URIs" do
52
+ n('http://igvita.com/').should == igvita
53
+ n('http://igvita.com').to_s.should == igvita
54
+ n('http://igvita.com///').should == igvita
55
+
56
+ n('http://igvita.com/../').should == igvita
57
+ n('http://igvita.com/a/b/../../').should == igvita
58
+ n('http://igvita.com/a/b/../..').should == igvita
59
+ end
60
+
61
+ it "should normalize query strings in PostRank::URIs" do
62
+ n('http://igvita.com/?').should == igvita
63
+ n('http://igvita.com?').should == igvita
64
+ n('http://igvita.com/a/../?').should == igvita
65
+ end
66
+
67
+ it "should normalize anchors in PostRank::URIs" do
68
+ n('http://igvita.com#test').should == igvita
69
+ n('http://igvita.com#test#test').should == igvita
70
+ n('http://igvita.com/a/../?#test').should == igvita
71
+ end
72
+
73
+ it "should clean whitespace in PostRank::URIs" do
74
+ n('http://igvita.com/a/../? ').should == igvita
75
+ n('http://igvita.com/a/../? #test').should == igvita
76
+ n('http://igvita.com/ /../').should == igvita
77
+ end
78
+
79
+ it "should default to http scheme if missing" do
80
+ n('igvita.com').should == igvita
81
+ n('https://test.com/').to_s.should == 'https://test.com/'
82
+ end
83
+
84
+ it "should downcase hostname" do
85
+ n('IGVITA.COM').should == igvita
86
+ n('IGVITA.COM/ABC').should == (igvita + "ABC")
87
+ end
88
+
89
+ end
90
+
91
+ context "canonicalization" do
92
+ def c(uri)
93
+ PostRank::URI.c18n(uri).to_s
94
+ end
95
+
96
+ context "query parameters" do
97
+ it "should handle nester parameters" do
98
+ c('igvita.com/?id=a&utm_source=a').should == 'http://igvita.com/?id=a'
99
+ end
100
+
101
+ it "should preserve order of parameters" do
102
+ url = 'http://a.com/?'+('a'..'z').to_a.shuffle.map {|e| "#{e}=#{e}"}.join("&")
103
+ c(url).should == url
104
+ end
105
+
106
+ it "should remove Google Analytics parameters" do
107
+ c('igvita.com/?id=a&utm_source=a').should == 'http://igvita.com/?id=a'
108
+ c('igvita.com/?id=a&utm_source=a&utm_valid').should == 'http://igvita.com/?id=a&utm_valid'
109
+ end
110
+
111
+ it "should remove awesm/sms parameters" do
112
+ c('igvita.com/?id=a&utm_source=a&awesm=b').should == 'http://igvita.com/?id=a'
113
+ c('igvita.com/?id=a&sms_ss=a').should == 'http://igvita.com/?id=a'
114
+ end
115
+
116
+ end
117
+ end
118
+
119
+ context "clean" do
120
+
121
+ def c(uri)
122
+ PostRank::URI.clean(uri)
123
+ end
124
+
125
+ it "should unescape, c18n and normalize" do
126
+ c('http://igvita.com/?id=1').should == 'http://igvita.com/?id=1'
127
+ c('igvita.com/?id=1').should == 'http://igvita.com/?id=1'
128
+
129
+ c('http://igvita.com/?id= 1').should == 'http://igvita.com/?id=%201'
130
+ c('http://igvita.com/?id=+1').should == 'http://igvita.com/?id=%201'
131
+ c('http://igvita.com/?id%3D%201').should == 'http://igvita.com/?id=%201'
132
+
133
+ c('igvita.com/a/..?id=1&utm_source=a&awesm=b#c').should == 'http://igvita.com/?id=1'
134
+
135
+ c('igvita.com?id=<>').should == 'http://igvita.com/?id=%3C%3E'
136
+ c('igvita.com?id="').should == 'http://igvita.com/?id=%22'
137
+ end
138
+
139
+ it "should clean host specific parameters" do
140
+ YAML.load_file('spec/c18n_hosts.yml').each do |orig, clean|
141
+ c(orig).should == clean
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ context "extract" do
148
+ def e(text)
149
+ PostRank::URI.extract(text)
150
+ end
151
+
152
+ context "TLDs" do
153
+ it "should not pick up bad grammar as a domain name and think it has a link" do
154
+ e("yah.lets").should be_empty
155
+ end
156
+
157
+ it "should not pickup bad TLDS" do
158
+ e('stuff.zz a.b.c d.zq').should be_empty
159
+ end
160
+ end
161
+
162
+ it "should handle a URL that comes after text without a space" do
163
+ e("text:http://spn.tw/tfnLT").should include("http://spn.tw/tfnLT")
164
+ e("text;http://spn.tw/tfnLT").should include("http://spn.tw/tfnLT")
165
+ e("text.http://spn.tw/tfnLT").should include("http://spn.tw/tfnLT")
166
+ e("text-http://spn.tw/tfnLT").should include("http://spn.tw/tfnLT")
167
+ end
168
+
169
+ it "should not pick up anything on or after the first . in the path of a URL with a shortener domain" do
170
+ e("http://bit.ly/9cJ2mz......if ur pickin up anythign here, u FAIL.").should == ["http://bit.ly/9cJ2mz"]
171
+ end
172
+
173
+ it "should pickup urls without protocol" do
174
+ u = e('abc.com abc.co')
175
+ u.should include('http://abc.com/')
176
+ u.should include('http://abc.co/')
177
+ end
178
+
179
+ context "multibyte characters" do
180
+ it "should stop extracting URLs at the full-width CJK space character" do
181
+ e("http://www.youtube.com/watch?v=w_j4Lda25jA  とんかつ定食").should == ["http://www.youtube.com/watch?v=w_j4Lda25jA"]
182
+ end
183
+ end
184
+ end
185
+
186
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: postrank-uri
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Ilya Grigorik
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-01-20 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: addressable
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: domainatrix
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ version: "0"
44
+ type: :runtime
45
+ version_requirements: *id002
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ prerelease: false
49
+ requirement: &id003 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ type: :development
58
+ version_requirements: *id003
59
+ description: URI normalization, c18n, escaping, and extraction
60
+ email:
61
+ - ilya@igvita.com
62
+ executables: []
63
+
64
+ extensions: []
65
+
66
+ extra_rdoc_files: []
67
+
68
+ files:
69
+ - Gemfile
70
+ - README.md
71
+ - Rakefile
72
+ - lib/postrank-uri.rb
73
+ - lib/postrank-uri/c18n.yml
74
+ - lib/postrank-uri/version.rb
75
+ - postrank-uri.gemspec
76
+ - spec/c18n_hosts.yml
77
+ - spec/helper.rb
78
+ - spec/postrank-uri_spec.rb
79
+ has_rdoc: true
80
+ homepage: http://rubygems.org/gems/postrank-uri
81
+ licenses: []
82
+
83
+ post_install_message:
84
+ rdoc_options: []
85
+
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ requirements: []
105
+
106
+ rubyforge_project: postrank-uri
107
+ rubygems_version: 1.3.7
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: URI normalization, c18n, escaping, and extraction
111
+ test_files:
112
+ - spec/c18n_hosts.yml
113
+ - spec/helper.rb
114
+ - spec/postrank-uri_spec.rb