linkhum 0.0.2 → 0.0.3

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