linkhum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. checksums.yaml +15 -0
  2. data/.dokaz +1 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +191 -0
  5. data/lib/linkhum.rb +111 -0
  6. data/linkhum.gemspec +32 -0
  7. metadata +134 -0
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NDZmOTk1OTg3ZjU2MzYxMTc2ZTA1ZDM1YjM4NmI2NWJlYzdkMWUyYQ==
5
+ data.tar.gz: !binary |-
6
+ MmFjMWJlOTk3YjM5MGEyOTkzNWE2Y2E5NzYwZTFmM2U1Yjc5MjQwNw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZGE1YTI2MzFmY2E3OGY4OTJkNTg2Mjk5YzI3ZTFjZDAzNmFkNmI0MTMyMzVh
10
+ MTJjMjgzZGU0MjYxMjZiNWRkYTZmYjYzOGVhY2U1ZmU0NWU4MzI3ZjQ5YzIx
11
+ MjI1MTU4ZTM4NTAyMjc1N2Y3MzU2NGE2NGQzZGFhNjdlOTZkYjI=
12
+ data.tar.gz: !binary |-
13
+ NzcwZjRmNDcwYTc4OTVjNTY4OTY2NTQ5OTg3NWIwZjZhOGU5MWFkOTExNGVm
14
+ MThkNDAzYmFlMjRkYTkwNWNiMWE0MTk3NWY1NzBmM2U4NGQ5NjY0ODA2MmFi
15
+ NGRjMGFkNzM0ZjY2NjU3NzRhZDNkMjEwNzYyMWI5YmZkYWJiYjY=
data/.dokaz ADDED
@@ -0,0 +1 @@
1
+ --require ./spec/dokaz_helper
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014-15 Victor 'Zverok' Shepelev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,191 @@
1
+ # LinkHum
2
+
3
+ **LinkHum** (aka "Links Humana") is URL auto-linker for user-entered texts.
4
+ It tries hard to do the most reasonable thing even in complex cases.
5
+
6
+ It will be useful for sites with plain-text user input
7
+
8
+ Features:
9
+ * auto-links URL;
10
+ * very accurate detection of punctiations inside and outside of URL;
11
+ * excessive tests set for complex (yet real-life) texts with URLs;
12
+ * customizable behavior.
13
+
14
+ **NB**: the original algo was written by [squadette](https://github.com/squadette)
15
+ and the test cases provided by users of _some secret social network_.
16
+ Just gemifying this (on behalf of original author).
17
+
18
+ ## Install
19
+
20
+ ```
21
+ [sudo] gem install linkhum
22
+ ```
23
+
24
+ Or in your Gemfile
25
+
26
+ ```ruby
27
+ gem 'linkhum'
28
+ ```
29
+
30
+ And then
31
+
32
+ ```
33
+ bundle install
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ As simple as:
39
+
40
+ ```ruby
41
+ LinkHum.urlify("Please look at http://github.com/zverok/linkhum, it's awesome!")
42
+ # => 'Please look at <a href="http://github.com/zverok/linkhum">http://github.com/zverok/linkhum</a>, it's awesome!'
43
+ ```
44
+
45
+ ## Showcase
46
+
47
+ ```ruby
48
+ # Doesn't touch punctuations outside:
49
+ LinkHum.urlify('http://slashdot.org, or http://lwn.net? They say, "just http://google.com"')
50
+ # => "<a href='http://slashdot.org'>http://slashdot.org</a>, or <a href='http://lwn.net'>http://lwn.net</a>? They say, \"just <a href='http://google.com'>http://google.com</a>\""
51
+
52
+ # But processes it inside:
53
+ LinkHum.urlify('Watch this: https://www.youtube.com/watch?v=Q9Dv4Hmf_O8')
54
+ # => "Watch this: <a href='https://www.youtube.com/watch?v=Q9Dv4Hmf_O8'>https://www.youtube.com/watch?v=Q9Dv4Hmf_O8</a>"
55
+
56
+ # Understands parentheses:
57
+ LinkHum.urlify("It's a movie: https://en.wikipedia.org/wiki/Hours_(2013_film) It's just parens: (https://www.youtube.com/watch?v=Q9Dv4Hmf_O8)")
58
+ # => "It's a movie: <a href='https://en.wikipedia.org/wiki/Hours_(2013_film)'>https://en.wikipedia.org/wiki/Hours_(2013_film)</a> It's just parens: (<a href='https://www.youtube.com/watch?v=Q9Dv4Hmf_O8'>https://www.youtube.com/watch?v=Q9Dv4Hmf_O8</a>)"
59
+
60
+ # URL shortening:
61
+ LinkHum.urlify("It's too long: http://www.booking.com/searchresults.ru.html?sid=28c7356c8d0fb6d81de3a45eff97e0fe;dcid=4;bb_asr=2&class_interval=1&csflt=%7B%7D&dest_id=-2167973&dest_type=city&group_adults=2&group_children=0&idf=1&label_click=undef&no_rooms=1&offset=0&review_score_group=empty&score_min=0&si=ai%2Cco%2Cci%2Cre%2Cdi&src=index&ss=Lisbon%2C%20Lisbon%20Region%2C%20Portugal&ss_raw=Lisbon&ssb=empty")
62
+ # => "It's too long: <a href='http://www.booking.com/searchresults.ru.html?sid=28c7356c8d0fb6d81de3a45eff97e0fe;dcid=4;bb_asr=2&class_interval=1&csflt=%7B%7D&dest_id=-2167973&dest_type=city&group_adults=2&group_children=0&idf=1&label_click=undef&no_rooms=1&offset=0&review_score_group=empty&score_min=0&si=ai,co,ci,re,di&src=index&ss=Lisbon,%20Lisbon%20Region,%20Portugal&ss_raw=Lisbon&ssb=empty'>http://www.booking.com/searchresults.ru.html?sid=28c7356c8d0f...</a>"
63
+
64
+ # It's customizable:
65
+ LinkHum.urlify(
66
+ "It's too long: http://www.booking.com/searchresults.ru.html?sid=28c7356c8d0fb6d81de3a45eff97e0fe;dcid=4;bb_asr=2&class_interval=1&csflt=%7B%7D&dest_id=-2167973&dest_type=city&group_adults=2&group_children=0&idf=1&label_click=undef&no_rooms=1&offset=0&review_score_group=empty&score_min=0&si=ai%2Cco%2Cci%2Cre%2Cdi&src=index&ss=Lisbon%2C%20Lisbon%20Region%2C%20Portugal&ss_raw=Lisbon&ssb=empty",
67
+ max_length: 20)
68
+ # =>
69
+
70
+ # International domains and Non-ASCII paths:
71
+ LinkHum.urlify("Domain: http://www.詹姆斯.com/, and path: https://ru.wikipedia.org/wiki/Эффект_Даннинга_—_Крюгера")
72
+ # => "Domain: <a href='http://www.詹姆斯.com/'>http://www.詹姆斯.com/</a>, and path: <a href='https://ru.wikipedia.org/wiki/%D0%AD%D1%84%D1%84%D0%B5%D0%BA%D1%82_%D0%94%D0%B0%D0%BD%D0%BD%D0%B8%D0%BD%D0%B3%D0%B0_%E2%80%94_%D0%9A%D1%80%D1%8E%D0%B3%D0%B5%D1%80%D0%B0'>https://ru.wikipedia.org/wiki/Эффект_Даннинга_—_Крюгера</a>"
73
+
74
+ # Look, ma, no XSS!
75
+ LinkHum.urlify('http://example.com/foo?">here.</a><script>window.alert("wow");</script>')
76
+ # => "<a href='http://example.com/foo?%22%3Ehere.%3C/a%3E%3Cscript%3Ewindow.alert(%22wow%22);%3C/script%3E'>http://example.com/foo?\">here.</a><script>window.alert(\"wow\")...</a>"
77
+ ```
78
+
79
+ ## Customization
80
+
81
+ ### On the fly
82
+
83
+ Custom URL params:
84
+
85
+ ```ruby
86
+ LinkHum.urlify("http://oursite.com/posts/12345 has been mentioned at http://cnn.com"){
87
+ |uri|
88
+ uri.host == 'oursite.com' ? {} : {target: '_blank'}
89
+ }
90
+ # => "<a href='http://oursite.com/posts/12345'>http://oursite.com/posts/12345</a> has been mentioned at <a href='http://cnn.com' target='_blank'>http://cnn.com</a>"
91
+ ```
92
+
93
+ Provided block should receive an instance of `Addressable::URI` and
94
+ return hash of additional link attributes. You can use it for opening
95
+ foreign links in new tab, or for styling them different (Wikipedia-style),
96
+ or to provide special icons for links to Youtube, Wikipedia and Google...
97
+ Up to you
98
+
99
+ ### Define your own LinkHum
100
+
101
+ ```ruby
102
+ class MyLinks < LinkHum
103
+ def link_attrs(uri)
104
+ {target: '_blank'} unless uri.host == 'oursite.com'
105
+ end
106
+ end
107
+
108
+ MyLinks.urlify("http://oursite.com/posts/12345 has been mentioned at http://cnn.com")
109
+ # => "<a href='http://oursite.com/posts/12345'>http://oursite.com/posts/12345</a> has been mentioned at <a href='http://cnn.com' target='_blank'>http://cnn.com</a>"
110
+ ```
111
+
112
+ You can also define special strings, which should also became URLs on your
113
+ site:
114
+
115
+ ```ruby
116
+ class MyLinks < LinkHum
117
+ special /@(\S+)\b/ do |username|
118
+ "http://oursite/users/#{username}"
119
+ end
120
+ end
121
+
122
+ MyLinks.urlify("Hey, @jude!")
123
+ # => "Hey, <a href='http://oursite/users/jude'>@jude</a>!"
124
+
125
+ # nil or false means no replacements:
126
+ class MyLinks < LinkHum
127
+ special /@(\S+)\b/ do |username|
128
+ "http://oursite/users/#{username}" if User.where(name: username).exists?
129
+ end
130
+ end
131
+
132
+ MyLinks.urlify("So, our @dude and @unknownguy walk into a bar...")
133
+ # => "So, our <a href='http://oursite/users/dude'>@dude</a> and @unknownguy walk into a bar..."
134
+ ```
135
+
136
+ Some `special` gotchas:
137
+ * for now, only one `special` per class is supported (an attempt to define
138
+ additional one will show warningn);
139
+ * it passes to the block values by the same logic as `String#scan` does:
140
+
141
+ ```ruby
142
+ class AllSymbols < LinkHum
143
+ special /@\S+\b/ do |username|
144
+ p username
145
+ nil
146
+ end
147
+ end
148
+ AllSymbols.urlify('@dude')
149
+ # Receives "@dude"
150
+
151
+ class SelectedPart < LinkHum
152
+ special /@(\S+)\b/ do |username|
153
+ p username
154
+ nil
155
+ end
156
+ end
157
+ SelectedPart.urlify('@dude')
158
+ # Receives "dude"
159
+
160
+ class SeveralArgs < LinkHum
161
+ special(/@(\S+)_(\S+)\b/) do |first, second|
162
+ p first, second
163
+ nil
164
+ end
165
+ end
166
+ SeveralArgs.urlify('@cool_dude')
167
+ # Receives "cool", "dude"
168
+ ```
169
+
170
+ ## Credits
171
+
172
+ * [squadette](https://github.com/squadette) -- author of original code;
173
+ * users of _some secret social network_ -- testing and advicing;
174
+ * [zverok](https://github.com/zverok) -- gemifying, documenting and
175
+ writing specs.
176
+
177
+ ## Contributing
178
+
179
+ Just usual fork-change-pull request process.
180
+
181
+ ### Development
182
+
183
+ * Don't forget to use `rspec` after any changes made (and specify them,
184
+ of course!)
185
+ * It's preferred to use `bundle exec dokaz` to check if README written
186
+ correctly and `bundle exec dokaz -fshow` to check what exactly code
187
+ from README will output.
188
+
189
+ ## License
190
+
191
+ MIT
@@ -0,0 +1,111 @@
1
+ # encoding: utf-8
2
+ require 'addressable/uri'
3
+ require 'cgi'
4
+
5
+ class LinkHum
6
+ class << self
7
+ NOOP = ->(*){}
8
+
9
+ def urlify(text, options = {}, &block)
10
+ new(text).urlify(options.merge(link_processor: block))
11
+ end
12
+
13
+ def special(pattern = nil, &block)
14
+ return @special unless pattern
15
+
16
+ @special and
17
+ puts("Warning: redefining #{self}.special from #{caller.first}")
18
+
19
+ @special = [pattern, block]
20
+ end
21
+ end
22
+
23
+ PROTOCOLS = '(?:https?|ftp)'
24
+ SPLIT_PATTERN = %r{(#{PROTOCOLS}://\p{^Space}+)}i
25
+
26
+ MAX_DISPLAY_LENGTH = 64
27
+
28
+ def initialize(text)
29
+ @text = text
30
+ @components = @text.split(SPLIT_PATTERN)
31
+ end
32
+
33
+ def urlify(options = {})
34
+ @components.map{|str|
35
+ SPLIT_PATTERN =~ str ? process_url(str, options) : process_text(str)
36
+ }.join
37
+ end
38
+
39
+ private
40
+
41
+ def process_url(str, options)
42
+ url, punct = str.scan(%r{\A(#{PROTOCOLS}://.+?)(\p{Punct}*)\Z}i).flatten
43
+ return str unless url
44
+
45
+ if punct[0] == '/' || (punct[0] == ')' && url.include?('('))
46
+ url << punct.slice!(0)
47
+ end
48
+
49
+ make_link(url, options) + punct
50
+ end
51
+
52
+ def process_text(str)
53
+ str = CGI.escapeHTML(str)
54
+
55
+ pattern, block = self.class.special
56
+
57
+ if pattern
58
+ str.gsub(pattern){|s|
59
+ if (u = block.call(*arguments(pattern, s)))
60
+ "<a href='#{screen_feet(u)}'>#{s}</a>"
61
+ else
62
+ s
63
+ end
64
+ }
65
+ else
66
+ str
67
+ end
68
+ end
69
+
70
+ def arguments(pattern, string)
71
+ m = pattern.match(string)
72
+ m.captures.empty? ? m[0] : m.captures
73
+ end
74
+
75
+ def make_link(url, options)
76
+ uri = Addressable::URI.parse(url) rescue nil
77
+ return url unless uri
78
+
79
+ canonical = Addressable::URI.normalized_encode(uri) rescue uri
80
+
81
+ display_length = options.fetch(:max_length, MAX_DISPLAY_LENGTH)
82
+ "<a href='#{screen_feet(canonical)}'#{make_attrs(uri, options)}>"\
83
+ "#{truncate(CGI.escapeHTML(url), display_length)}</a>"
84
+ end
85
+
86
+ def make_attrs(uri, options)
87
+ block = options[:link_processor] || method(:link_attrs)
88
+ attrs = block.call(uri) || {}
89
+ return '' if attrs.empty?
90
+ ' ' + attrs.map{|n, v| "#{n}='#{v}'"}.join(' ')
91
+ end
92
+
93
+ def link_attrs(*)
94
+ end
95
+
96
+ # TIL that ' (single quote) is in fact "feet mark"
97
+ def screen_feet(url)
98
+ url.gsub("'", '%27')
99
+ end
100
+
101
+ # stolen from activesupport/lib/active_support/core_ext/string/filters.rb
102
+ # then simplified
103
+ def truncate(string, truncate_at)
104
+ return string.dup if !truncate_at || string.length <= truncate_at
105
+
106
+ omission = '...'
107
+ stop = truncate_at - omission.length
108
+
109
+ "#{string[0, stop]}#{omission}"
110
+ end
111
+ end
@@ -0,0 +1,32 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'linkhum'
3
+ s.version = '0.0.1'
4
+ s.authors = ['Alexey Makhotkin', 'Victor Shepelev']
5
+ s.email = 'zverok.offline@gmail.com'
6
+ s.homepage = 'https://github.com/zverok/linkhum'
7
+
8
+ s.summary = 'Humane link urlifier'
9
+ s.licenses = ['MIT']
10
+
11
+ s.files = `git ls-files`.split($RS).reject do |file|
12
+ file =~ /^(?:
13
+ spec\/.*
14
+ |Gemfile
15
+ |Rakefile
16
+ |\.rspec
17
+ |\.gitignore
18
+ |\.rubocop.yml
19
+ |\.travis.yml
20
+ )$/x
21
+ end
22
+ s.require_paths = ["lib"]
23
+
24
+ s.add_dependency 'addressable'
25
+
26
+ s.add_development_dependency 'rake'
27
+ s.add_development_dependency 'rspec', '~> 3'
28
+ s.add_development_dependency 'rspec-its', '~> 1'
29
+ s.add_development_dependency 'nokogiri'
30
+ s.add_development_dependency 'rubocop'
31
+ #s.add_development_dependency 'dokaz'
32
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linkhum
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexey Makhotkin
8
+ - Victor Shepelev
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-06-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: addressable
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ! '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: '3'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: '3'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec-its
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '1'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1'
70
+ - !ruby/object:Gem::Dependency
71
+ name: nokogiri
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rubocop
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ description:
99
+ email: zverok.offline@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .dokaz
105
+ - LICENSE.txt
106
+ - README.md
107
+ - lib/linkhum.rb
108
+ - linkhum.gemspec
109
+ homepage: https://github.com/zverok/linkhum
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 2.4.6
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Humane link urlifier
133
+ test_files: []
134
+ has_rdoc: