linkhum 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +5 -13
  2. data/README.md +30 -2
  3. data/lib/linkhum.rb +87 -37
  4. data/linkhum.gemspec +2 -1
  5. metadata +31 -18
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MjZjODJiMGM1N2Q2ZjdlOTkyN2IyMThkNGQ4MDI3NjgyMzJhYzExZA==
5
- data.tar.gz: !binary |-
6
- M2MwNTM2MGVmZjk5ZmZiMzMyOGU4YWFkN2Q2NDUyY2M0ODJlZWUzOA==
2
+ SHA1:
3
+ metadata.gz: ad631369a4a44fc5562c929ebc4a8fa923e745a5
4
+ data.tar.gz: 184172a484e5eb074c8a28d269225ada3bd9c27f
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- OTY5ZGIzNzdkZjgzYWJkODcyYjJlZjYzMzM4NGRiNGViYjNhMjUxNTdmMTM0
10
- NDNhMTY2NzE4NzNlMDY4ZWQ2NTA4OTE4ZmU0YzIyZGY1ZWY4Y2Y0NjZkNWFj
11
- YTdkZGY1N2U3NmMzMTA0NjU2ZTlkYWRlYTgyZTcwNWI0NGU2NzA=
12
- data.tar.gz: !binary |-
13
- YWMwMDdhMzZjNzcxMDMyMTlhN2E0MmRkMGY1NDBmMzA0N2UwYWRjMjZjNGU5
14
- MjljNGNmZTkxOTIxMDJlMjlkN2ZjZmY1MDhkODI0MjhmZTIyZjRmMWE0MzRm
15
- N2NkMTM1OWQxYzQ1YTY1ZGFiYTlhODY3OWNhOTIwNzdhNGI5NDQ=
6
+ metadata.gz: bef81a104f120fcb54de689365516c1d4211e45652c299f024377c406234fc26eaec9c4924e8ff646b54d65c3e96d3a5c82d2a3d06d5e5ec675a46f53492863a
7
+ data.tar.gz: ad5e85a10b6c8a580fc8a58988fd13f891fce000255da24de010a8756ae8743fb98d85b4a583047615567cb3628ebcc7d8effe6171d1e5f9571ba903f8889c8b
data/README.md CHANGED
@@ -12,7 +12,7 @@ Features:
12
12
  * customizable behavior.
13
13
 
14
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_.
15
+ and the test cases provided by users of [Mokum](https://mokum.place).
16
16
  Just gemifying this (on behalf of original author).
17
17
 
18
18
  ## Install
@@ -167,10 +167,38 @@ SeveralArgs.urlify('@cool_dude')
167
167
  # Receives "cool", "dude"
168
168
  ```
169
169
 
170
+ ### "Parse only" mode
171
+
172
+ If your demands for resulting strings construction is far more complicated
173
+ than default LinkHum behavior, you can use its `#parse` command to split
174
+ string into tokens, and process them by yourself. All URL-detection
175
+ goodness and `special`s still will be with you:
176
+
177
+ ```ruby
178
+ class MyParser < LinkHum
179
+ # You don't need rendering blocks for your specials
180
+ # Second argument is special's name, it is optional
181
+ special /@(\S+)\b/, :username
182
+ special /\#(\S+)\b/, :tag
183
+ end
184
+
185
+ MyParser.parse("Here is @dude. He is #cute. Is he on http://facebook.com?")
186
+ # => [
187
+ # {type: :text , content: 'Here is '},
188
+ # {type: :username, content: '@dude', captures: ['dude']},
189
+ # {type: :text , content: '. He is '},
190
+ # {type: :tag , content: '#cute', captures: ['cute']},
191
+ # {type: :text , content: '. Is he on '},
192
+ # {type: :url , content: 'http://facebook.com'},
193
+ # {type: :text , content: '?'}
194
+ # ]
195
+ ```
196
+
170
197
  ## Credits
171
198
 
172
199
  * [squadette](https://github.com/squadette) -- author of original code;
173
- * users of _some secret social network_ -- testing and advicing;
200
+ * users of [Mokum](https://mokum.place) -- testing and advicing (and now
201
+ you can observe LinkHum work online at Mokum);
174
202
  * [zverok](https://github.com/zverok) -- gemifying, documenting and
175
203
  writing specs.
176
204
 
data/lib/linkhum.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
  require 'addressable/uri'
3
3
  require 'cgi'
4
+ require 'strscan'
4
5
 
5
6
  class LinkHum
6
7
  class << self
@@ -10,76 +11,125 @@ class LinkHum
10
11
  new(text).urlify(options.merge(link_processor: block))
11
12
  end
12
13
 
14
+ def parse(text)
15
+ new(text).parse
16
+ end
17
+
13
18
  def specials
14
- @specials ||= []
19
+ @specials ||= {}
15
20
  end
16
21
 
17
- def special(pattern = nil, &block)
18
- specials << [pattern, block]
22
+ RESERVED_NAMES = [:text, :url, :special]
23
+
24
+ def special(pattern, name = nil, &block)
25
+ name ||= "special_#{specials.count + 1}".to_sym
26
+
27
+ RESERVED_NAMES.include?(name) and fail(ArgumentError, "#{name} is reserved")
28
+ specials.key?(name) and fail(ArgumentError, "#{name} is already defined")
29
+
30
+ specials[name] = [pattern, block]
19
31
  end
20
32
  end
21
33
 
22
34
  PROTOCOLS = '(?:https?|ftp)'
23
- SPLIT_PATTERN = %r{(#{PROTOCOLS}://\p{^Space}+)}i
35
+ URL_PATTERN = %r{(#{PROTOCOLS}://\p{^Space}+)}i
24
36
 
25
37
  MAX_DISPLAY_LENGTH = 64
26
38
 
27
39
  def initialize(text)
28
40
  @text = text
29
- @components = @text.split(SPLIT_PATTERN)
41
+ end
42
+
43
+ def parse
44
+ (@text.split(URL_PATTERN) + ['']).tap{|components|
45
+ (components).each_cons(2){|left, right|
46
+ # ['http://google.com', '/ and stuff'] => ['http://google.com/', ' and stuff']
47
+ shift_punct(left, right) if url?(left) && !url?(right)
48
+ }
49
+ }.reject(&:empty?).
50
+ map{|str|
51
+ url?(str) ? {type: :url, content: str} : parse_specials(str)
52
+ }.flatten
30
53
  end
31
54
 
32
55
  def urlify(options = {})
33
- @components.map{|str|
34
- SPLIT_PATTERN =~ str ? process_url(str, options) : process_text(str)
56
+ parse.map{|component|
57
+ case component[:type]
58
+ when :url
59
+ process_url(component[:content], options)
60
+ when :text
61
+ process_text(component[:content])
62
+ else
63
+ process_special(component)
64
+ end
35
65
  }.join
36
66
  end
37
67
 
38
68
  private
39
69
 
40
- def process_url(str, options)
41
- url, punct = str.scan(%r{\A(#{PROTOCOLS}://.+?)(\p{Punct}*)\Z}i).flatten
42
- return str unless url
43
-
70
+ def url?(str)
71
+ str =~ URL_PATTERN
72
+ end
73
+
74
+ # NB: nasty inplace strings changing is going on inside, beware!
75
+ def shift_punct(url, text_after)
76
+ url_, punct = url.scan(%r{\A(#{PROTOCOLS}://.+?)(\p{Punct}*)\Z}i).flatten
77
+ return unless url_
78
+
44
79
  if punct[0] == '/' || (punct[0] == ')' && url.include?('('))
45
- url << punct.slice!(0)
80
+ url_ << punct.slice!(0)
46
81
  end
47
82
 
48
- make_link(url, options) + punct
83
+ url.replace(url_)
84
+ text_after.prepend(punct)
49
85
  end
50
86
 
51
- def process_text(str)
52
- str = CGI.escapeHTML(str)
53
-
54
- if self.class.specials.empty?
55
- str
56
- else
57
- replace_specials(str)
87
+ def parse_specials(str)
88
+ res = []
89
+ str = str.dup
90
+ while !str.empty?
91
+ md = (specials_pattern.match(str)) or break
92
+ md.length.zero? and fail(RuntimeError, "Empty string matched by special at '#{str}'")
93
+
94
+ res << {type: :text, content: md.pre_match}
95
+ res << {type: :special, content: md[0]}
96
+ str = md.post_match
58
97
  end
98
+ res << {type: :text, content: str}
99
+ res.reject{|r| r[:content].empty?}.each{|r|
100
+ update_special(r) if r[:type] == :special
101
+ }
59
102
  end
60
103
 
61
- def replace_specials(str)
62
- patterns = self.class.specials.map(&:first)
63
- blocks = self.class.specials.map(&:last)
64
-
65
- str.gsub(Regexp.union(patterns)) do |s|
66
- pattern = patterns.detect{|p| s[p] == s}
67
- idx = patterns.index(pattern)
68
-
69
- if idx && (u = blocks[idx].call(*arguments(pattern, s)))
70
- "<a href='#{screen_feet(u)}'>#{s}</a>"
71
- else
72
- s
73
- end
104
+ def specials_pattern
105
+ @specials_pattern ||= Regexp.union(self.class.specials.values.map(&:first))
106
+ end
107
+
108
+ def update_special(hash)
109
+ str = hash[:content]
110
+ name, (pattern, block) = self.class.specials.detect{|n, (p, b)| str[p] == str}
111
+ if name
112
+ hash.update(type: name, captures: pattern.match(str).captures)
74
113
  end
75
114
  end
76
115
 
77
- def arguments(pattern, string)
78
- m = pattern.match(string)
79
- m.captures.empty? ? m[0] : m.captures
116
+ def process_text(str)
117
+ CGI.escapeHTML(str)
118
+ end
119
+
120
+ def process_special(special)
121
+ return special[:content] if special[:type] == :special
122
+
123
+ _pattern, block = self.class.specials[special[:type]]
124
+ args = special[:captures] || [special[:match]]
125
+ if u = block.call(*args)
126
+ "<a href='#{screen_feet(u)}'>#{special[:content]}</a>"
127
+ else
128
+ special[:content]
129
+ end
80
130
  end
81
131
 
82
- def make_link(url, options)
132
+ def process_url(url, options)
83
133
  uri = Addressable::URI.parse(url) rescue nil
84
134
  return url unless uri
85
135
 
data/linkhum.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'linkhum'
3
- s.version = '0.0.2'
3
+ s.version = '0.0.3'
4
4
  s.authors = ['Alexey Makhotkin', 'Victor Shepelev']
5
5
  s.email = 'zverok.offline@gmail.com'
6
6
  s.homepage = 'https://github.com/zverok/linkhum'
@@ -28,5 +28,6 @@ Gem::Specification.new do |s|
28
28
  s.add_development_dependency 'rspec-its', '~> 1'
29
29
  s.add_development_dependency 'nokogiri'
30
30
  s.add_development_dependency 'rubocop'
31
+ s.add_development_dependency 'rubygems-tasks'
31
32
  #s.add_development_dependency 'dokaz'
32
33
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linkhum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Makhotkin
@@ -9,90 +9,104 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-07-16 00:00:00.000000000 Z
12
+ date: 2015-12-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: addressable
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - ! '>='
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
20
  version: '0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - ! '>='
25
+ - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: rake
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
- - - ! '>='
32
+ - - ">="
33
33
  - !ruby/object:Gem::Version
34
34
  version: '0'
35
35
  type: :development
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
- - - ! '>='
39
+ - - ">="
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: rspec
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - ~>
46
+ - - "~>"
47
47
  - !ruby/object:Gem::Version
48
48
  version: '3'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - ~>
53
+ - - "~>"
54
54
  - !ruby/object:Gem::Version
55
55
  version: '3'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rspec-its
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - ~>
60
+ - - "~>"
61
61
  - !ruby/object:Gem::Version
62
62
  version: '1'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - ~>
67
+ - - "~>"
68
68
  - !ruby/object:Gem::Version
69
69
  version: '1'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: nokogiri
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - ! '>='
74
+ - - ">="
75
75
  - !ruby/object:Gem::Version
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
- - - ! '>='
81
+ - - ">="
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  - !ruby/object:Gem::Dependency
85
85
  name: rubocop
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
- - - ! '>='
88
+ - - ">="
89
89
  - !ruby/object:Gem::Version
90
90
  version: '0'
91
91
  type: :development
92
92
  prerelease: false
93
93
  version_requirements: !ruby/object:Gem::Requirement
94
94
  requirements:
95
- - - ! '>='
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rubygems-tasks
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
96
110
  - !ruby/object:Gem::Version
97
111
  version: '0'
98
112
  description:
@@ -101,7 +115,7 @@ executables: []
101
115
  extensions: []
102
116
  extra_rdoc_files: []
103
117
  files:
104
- - .dokaz
118
+ - ".dokaz"
105
119
  - CHANGELOG.md
106
120
  - LICENSE.txt
107
121
  - README.md
@@ -117,12 +131,12 @@ require_paths:
117
131
  - lib
118
132
  required_ruby_version: !ruby/object:Gem::Requirement
119
133
  requirements:
120
- - - ! '>='
134
+ - - ">="
121
135
  - !ruby/object:Gem::Version
122
136
  version: '0'
123
137
  required_rubygems_version: !ruby/object:Gem::Requirement
124
138
  requirements:
125
- - - ! '>='
139
+ - - ">="
126
140
  - !ruby/object:Gem::Version
127
141
  version: '0'
128
142
  requirements: []
@@ -132,4 +146,3 @@ signing_key:
132
146
  specification_version: 4
133
147
  summary: Humane link urlifier
134
148
  test_files: []
135
- has_rdoc: