searchlink 2.3.59

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/bin/searchlink +84 -0
  3. data/lib/searchlink/array.rb +7 -0
  4. data/lib/searchlink/config.rb +230 -0
  5. data/lib/searchlink/curl/html.rb +482 -0
  6. data/lib/searchlink/curl/json.rb +90 -0
  7. data/lib/searchlink/curl.rb +7 -0
  8. data/lib/searchlink/help.rb +103 -0
  9. data/lib/searchlink/output.rb +270 -0
  10. data/lib/searchlink/parse.rb +668 -0
  11. data/lib/searchlink/plist.rb +213 -0
  12. data/lib/searchlink/search.rb +70 -0
  13. data/lib/searchlink/searches/amazon.rb +25 -0
  14. data/lib/searchlink/searches/applemusic.rb +123 -0
  15. data/lib/searchlink/searches/bitly.rb +50 -0
  16. data/lib/searchlink/searches/definition.rb +67 -0
  17. data/lib/searchlink/searches/duckduckgo.rb +167 -0
  18. data/lib/searchlink/searches/github.rb +245 -0
  19. data/lib/searchlink/searches/google.rb +67 -0
  20. data/lib/searchlink/searches/helpers/chromium.rb +318 -0
  21. data/lib/searchlink/searches/helpers/firefox.rb +135 -0
  22. data/lib/searchlink/searches/helpers/safari.rb +133 -0
  23. data/lib/searchlink/searches/history.rb +166 -0
  24. data/lib/searchlink/searches/hook.rb +77 -0
  25. data/lib/searchlink/searches/itunes.rb +97 -0
  26. data/lib/searchlink/searches/lastfm.rb +41 -0
  27. data/lib/searchlink/searches/lyrics.rb +91 -0
  28. data/lib/searchlink/searches/pinboard.rb +183 -0
  29. data/lib/searchlink/searches/social.rb +105 -0
  30. data/lib/searchlink/searches/software.rb +27 -0
  31. data/lib/searchlink/searches/spelling.rb +59 -0
  32. data/lib/searchlink/searches/spotlight.rb +28 -0
  33. data/lib/searchlink/searches/stackoverflow.rb +31 -0
  34. data/lib/searchlink/searches/tmdb.rb +52 -0
  35. data/lib/searchlink/searches/twitter.rb +46 -0
  36. data/lib/searchlink/searches/wikipedia.rb +33 -0
  37. data/lib/searchlink/searches/youtube.rb +48 -0
  38. data/lib/searchlink/searches.rb +194 -0
  39. data/lib/searchlink/semver.rb +140 -0
  40. data/lib/searchlink/string.rb +469 -0
  41. data/lib/searchlink/url.rb +153 -0
  42. data/lib/searchlink/util.rb +87 -0
  43. data/lib/searchlink/version.rb +93 -0
  44. data/lib/searchlink/which.rb +175 -0
  45. data/lib/searchlink.rb +66 -0
  46. data/lib/tokens.rb +3 -0
  47. metadata +299 -0
@@ -0,0 +1,140 @@
1
+ module SL
2
+ # Semantic versioning library
3
+ class SemVer
4
+ attr_accessor :maj, :min, :patch, :pre
5
+
6
+ # Initialize a Semantic Version object
7
+ #
8
+ # @param version_string [String] a semantic version number
9
+ #
10
+ # @return [SemVer] SemVer object
11
+ #
12
+ def initialize(version_string)
13
+ raise "Invalid semantic version number: #{version_string}" unless version_string.valid_version?
14
+
15
+ @maj, @min, @patch = version_string.split(/\./)
16
+ @pre = nil
17
+ if @patch =~ /(-?[^0-9]+\d*)$/
18
+ @pre = Regexp.last_match(1).sub(/^-/, '')
19
+ @patch = @patch.sub(/(-?[^0-9]+\d*)$/, '')
20
+ end
21
+
22
+ @maj = @maj.to_i
23
+ @min = @min.to_i
24
+ @patch = @patch.to_i
25
+ end
26
+
27
+ ##
28
+ ## SemVer String helpers
29
+ ##
30
+ class ::String
31
+ # Test if given string is a valid semantic version
32
+ # number with major, minor and patch (and optionally
33
+ # pre)
34
+ #
35
+ # @return [Boolean] string is semantic version number
36
+ #
37
+ def valid_version?
38
+ pattern = /^\d+\.\d+\.\d+(-?([^0-9]+\d*))?$/
39
+ self =~ pattern ? true : false
40
+ end
41
+ end
42
+
43
+ ##
44
+ ## Test if self is older than a semantic version number
45
+ ##
46
+ ## @param other [String,SemVer] The semantic version number or SemVer object
47
+ ##
48
+ ## @return [Boolean] true if semver is older
49
+ ##
50
+ def older_than(other)
51
+ latest = other.is_a?(SemVer) ? other : SemVer.new(other)
52
+
53
+ return false if latest.equal?(self)
54
+
55
+ if @maj > latest.maj
56
+ false
57
+ elsif @maj < latest.maj
58
+ true
59
+ elsif @min > latest.min
60
+ false
61
+ elsif @min < latest.min
62
+ true
63
+ elsif @patch > latest.patch
64
+ false
65
+ elsif @patch < latest.patch
66
+ true
67
+ else
68
+ return false if @pre.nil? && latest.pre.nil?
69
+
70
+ return true if @pre.nil? && !latest.pre.nil?
71
+
72
+ return false if !@pre.nil? && latest.pre.nil?
73
+
74
+ @pre < latest.pre
75
+ end
76
+ end
77
+
78
+ ##
79
+ ## @see #older_than
80
+ ##
81
+ def <(other)
82
+ older_than(other)
83
+ end
84
+
85
+ ##
86
+ ## Test if self is newer than a semantic version number
87
+ ##
88
+ ## @param other [String,SemVer] The semantic version
89
+ ## number or SemVer object
90
+ ##
91
+ ## @return [Boolean] true if semver is newer
92
+ ##
93
+ def newer_than(other)
94
+ v = other.is_a?(SemVer) ? other : SemVer.new(other)
95
+ v.older_than(self) && !v.equal?(self)
96
+ end
97
+
98
+ ##
99
+ ## @see #newer_than
100
+ ##
101
+ def >(other)
102
+ newer_than(other)
103
+ end
104
+
105
+ ##
106
+ ## Test if self is equal to other
107
+ ##
108
+ ## @param other [String,SemVer] The other semantic version number
109
+ ##
110
+ ## @return [Boolean] values are equal
111
+ ##
112
+ def equal?(other)
113
+ v = other.is_a?(SemVer) ? other : SemVer.new(other)
114
+
115
+ v.maj == @maj && v.min == @min && v.patch == @patch && v.pre == @pre
116
+ end
117
+
118
+ ##
119
+ ## @see #equal?
120
+ ##
121
+ def ==(other)
122
+ equal?(other)
123
+ end
124
+
125
+ def inspect
126
+ {
127
+ object_id: object_id,
128
+ maj: @maj,
129
+ min: @min,
130
+ patch: @patch,
131
+ pre: @pre
132
+ }
133
+ end
134
+
135
+ def to_s
136
+ ver = [@maj, @min, @patch].join('.')
137
+ @pre.nil? ? ver : "#{ver}-#{@pre}"
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,469 @@
1
+ # String helpers
2
+ class ::String
3
+ # URL Encode string
4
+ #
5
+ # @return [String] url encoded string
6
+ #
7
+ def url_encode
8
+ ERB::Util.url_encode(gsub(/%22/, '"'))
9
+ end
10
+
11
+ def url_decode
12
+ CGI.unescape(self)
13
+ end
14
+
15
+ ##
16
+ ## Adds ?: to any parentheticals in a regular expression
17
+ ## to avoid match groups
18
+ ##
19
+ ## @return [String] modified regular expression
20
+ ##
21
+ def normalize_trigger
22
+ gsub(/\((?!\?:)/, '(?:').gsub(/(^(\^|\\A)|(\$|\\Z)$)/, '').downcase
23
+ end
24
+
25
+ ##
26
+ ## Generate a spacer based on character widths for help dialog display
27
+ ##
28
+ ## @return [String] string containing tabs
29
+ ##
30
+ def spacer
31
+ len = length
32
+ scan(/[mwv]/).each { len += 1 }
33
+ scan(/t/).each { len -= 1 }
34
+ case len
35
+ when 0..3
36
+ "\t\t"
37
+ when 4..12
38
+ " \t"
39
+ end
40
+ end
41
+
42
+ # parse command line flags into long options
43
+ def parse_flags
44
+ gsub(/(\+\+|--)([dirtvs]+)\b/) do
45
+ m = Regexp.last_match
46
+ bool = m[1] == '++' ? '' : 'no-'
47
+ output = ' '
48
+ m[2].split('').each do |arg|
49
+ output += case arg
50
+ when 'd'
51
+ "--#{bool}debug "
52
+ when 'i'
53
+ "--#{bool}inline "
54
+ when 'r'
55
+ "--#{bool}prefix_random "
56
+ when 't'
57
+ "--#{bool}include_titles "
58
+ when 'v'
59
+ "--#{bool}validate_links "
60
+ when 's'
61
+ "--#{bool}remove_seo "
62
+ else
63
+ ''
64
+ end
65
+ end
66
+
67
+ output
68
+ end.gsub(/ +/, ' ')
69
+ end
70
+
71
+ def parse_flags!
72
+ replace parse_flags
73
+ end
74
+
75
+ ##
76
+ ## Convert file-myfile-rb to myfile.rb
77
+ ##
78
+ ## @return { description_of_the_return_value }
79
+ ##
80
+ def fix_gist_file
81
+ sub(/^file-/, '').sub(/-([^\-]+)$/, '.\1')
82
+ end
83
+
84
+ # Turn a string into a slug, removing spaces and
85
+ # non-alphanumeric characters
86
+ #
87
+ # @return [String] slugified string
88
+ #
89
+ def slugify
90
+ downcase.gsub(/[^a-z0-9_]/i, '-').gsub(/-+/, '-').sub(/-?$/, '')
91
+ end
92
+
93
+ # Destructive slugify
94
+ # @see #slugify
95
+ def slugify!
96
+ replace slugify
97
+ end
98
+
99
+ ##
100
+ ## Remove newlines, escape quotes, and remove Google
101
+ ## Analytics strings
102
+ ##
103
+ ## @return [String] cleaned URL/String
104
+ ##
105
+ def clean
106
+ gsub(/\n+/, ' ')
107
+ .gsub(/"/, '&quot')
108
+ .gsub(/\|/, '-')
109
+ .gsub(/([&?]utm_[scm].+=[^&\s!,.)\]]++?)+(&.*)/, '\2')
110
+ .sub(/\?&/, '').strip
111
+ end
112
+
113
+ # convert itunes to apple music link
114
+ #
115
+ # @return [String] apple music link
116
+ def to_am
117
+ input = dup
118
+ input.sub!(%r{/itunes\.apple\.com}, 'geo.itunes.apple.com')
119
+ append = input =~ %r{\?[^/]+=} ? '&app=music' : '?app=music'
120
+ input + append
121
+ end
122
+
123
+ ##
124
+ ## Remove the protocol from a URL
125
+ ##
126
+ ## @return [String] just hostname and path of URL
127
+ ##
128
+ def remove_protocol
129
+ sub(%r{^(https?|s?ftp|file)://}, '')
130
+ end
131
+
132
+ ##
133
+ ## Return just the path of a URL
134
+ ##
135
+ ## @return [String] The path.
136
+ ##
137
+ def url_path
138
+ URI.parse(self).path
139
+ end
140
+
141
+ # Extract the most relevant portions from a URL path
142
+ #
143
+ # @return [Array] array of relevant path elements
144
+ #
145
+ def path_elements
146
+ path = url_path
147
+ # force trailing slash
148
+ path.sub!(%r{/?$}, '/')
149
+ # remove last path element
150
+ path.sub!(%r{/[^/]+[.\-][^/]+/$}, '')
151
+ # remove starting/ending slashes
152
+ path.gsub!(%r{(^/|/$)}, '')
153
+ # split at slashes, delete sections that are shorter
154
+ # than 5 characters or only consist of numbers
155
+ path.split(%r{/}).delete_if { |section| section =~ /^\d+$/ || section.length < 5 }
156
+ end
157
+
158
+ ##
159
+ ## Destructive punctuation close
160
+ ##
161
+ ## @see #close_punctuation
162
+ ##
163
+ def close_punctuation!
164
+ replace close_punctuation
165
+ end
166
+
167
+ ##
168
+ ## Complete incomplete punctuation pairs
169
+ ##
170
+ ## @return [String] string with all punctuation
171
+ ## properly paired
172
+ ##
173
+ def close_punctuation
174
+ return self unless self =~ /[“‘\[(<]/
175
+
176
+ words = split(/\s+/)
177
+
178
+ punct_chars = {
179
+ '“' => '”',
180
+ '‘' => '’',
181
+ '[' => ']',
182
+ '(' => ')',
183
+ '<' => '>'
184
+ }
185
+
186
+ left_punct = []
187
+
188
+ words.each do |w|
189
+ punct_chars.each do |k, v|
190
+ left_punct.push(k) if w =~ /#{Regexp.escape(k)}/
191
+ left_punct.delete_at(left_punct.rindex(k)) if w =~ /#{Regexp.escape(v)}/
192
+ end
193
+ end
194
+
195
+ tail = ''
196
+ left_punct.reverse.each { |c| tail += punct_chars[c] }
197
+
198
+ gsub(/[^a-z)\]’”.…]+$/i, '...').strip + tail
199
+ end
200
+
201
+ ##
202
+ ## Destructively remove SEO elements from a title
203
+ ##
204
+ ## @param url The url of the page from which the
205
+ ## title came
206
+ ##
207
+ ## @see #remove_seo
208
+ ##
209
+ def remove_seo!(url)
210
+ replace remove_seo(url)
211
+ end
212
+
213
+ ##
214
+ ## Remove SEO elements from a title
215
+ ##
216
+ ## @param url The url of the page from which the title came
217
+ ##
218
+ ## @return [String] cleaned title
219
+ ##
220
+ def remove_seo(url)
221
+ title = dup
222
+ url = URI.parse(url)
223
+ host = url.hostname
224
+ unless host
225
+ return self unless SL.config['debug']
226
+
227
+ SL.add_error('Invalid URL', "Could not remove SEO for #{url}")
228
+ return self
229
+
230
+ end
231
+
232
+ path = url.path
233
+ root_page = path =~ %r{^/?$} ? true : false
234
+
235
+ title.gsub!(/\s*(&ndash;|&mdash;)\s*/, ' - ')
236
+ title.gsub!(/&[lr]dquo;/, '"')
237
+ title.gsub!(/&[lr]dquo;/, "'")
238
+ title.gsub!(/&#8211;/, ' — ')
239
+ title = CGI.unescapeHTML(title)
240
+ title.gsub!(/ +/, ' ')
241
+
242
+ seo_title_separators = %w[| » « — – - · :]
243
+
244
+ begin
245
+ re_parts = []
246
+
247
+ host_parts = host.sub(/(?:www\.)?(.*?)\.[^.]+$/, '\1').split(/\./).delete_if { |p| p.length < 3 }
248
+ h_re = !host_parts.empty? ? host_parts.map { |seg| seg.downcase.split(//).join('.?') }.join('|') : ''
249
+ re_parts.push(h_re) unless h_re.empty?
250
+
251
+ # p_re = path.path_elements.map{|seg| seg.downcase.split(//).join('.?') }.join('|')
252
+ # re_parts.push(p_re) if p_re.length > 0
253
+
254
+ site_re = "(#{re_parts.join('|')})"
255
+
256
+ dead_switch = 0
257
+
258
+ while title.downcase.gsub(/[^a-z]/i, '') =~ /#{site_re}/i
259
+
260
+ break if dead_switch > 5
261
+
262
+ seo_title_separators.each_with_index do |sep, i|
263
+ parts = title.split(/ *#{Regexp.escape(sep)} +/)
264
+
265
+ next if parts.length == 1
266
+
267
+ remaining_separators = seo_title_separators[i..].map { |s| Regexp.escape(s) }.join('')
268
+ seps = Regexp.new("^[^#{remaining_separators}]+$")
269
+
270
+ longest = parts.longest_element.strip
271
+
272
+ unless parts.empty?
273
+ parts.delete_if do |pt|
274
+ compressed = pt.strip.downcase.gsub(/[^a-z]/i, '')
275
+ compressed =~ /#{site_re}/ && pt =~ seps ? !root_page : false
276
+ end
277
+ end
278
+
279
+ title = if parts.empty?
280
+ longest
281
+ elsif parts.length < 2
282
+ parts.join(sep)
283
+ elsif parts.length > 2
284
+ parts.longest_element.strip
285
+ else
286
+ parts.join(sep)
287
+ end
288
+ end
289
+ dead_switch += 1
290
+ end
291
+ rescue StandardError => e
292
+ return self unless SL.config['debug']
293
+
294
+ SL.add_error("Error SEO processing title for #{url}", e)
295
+ return self
296
+ end
297
+
298
+ seps = Regexp.new(" *[#{seo_title_separators.map { |s| Regexp.escape(s) }.join('')}] +")
299
+ if title =~ seps
300
+ seo_parts = title.split(seps)
301
+ title = seo_parts.longest_element.strip if seo_parts.length.positive?
302
+ end
303
+
304
+ title && title.length > 5 ? title.gsub(/\s+/, ' ') : CGI.unescapeHTML(self)
305
+ end
306
+
307
+ ##
308
+ ## Truncate in place
309
+ ##
310
+ ## @see #truncate
311
+ ##
312
+ ## @param max [Number] The maximum length
313
+ ##
314
+ def truncate!(max)
315
+ replace truncate(max)
316
+ end
317
+
318
+ ##
319
+ ## Truncate string to given length, preserving words
320
+ ##
321
+ ## @param max [Number] The maximum length
322
+ ##
323
+ def truncate(max)
324
+ return self if length < max
325
+
326
+ trunc_title = []
327
+
328
+ words = split(/\s+/)
329
+ words.each do |word|
330
+ break unless trunc_title.join(' ').length.close_punctuation + word.length <= max
331
+
332
+ trunc_title << word
333
+ end
334
+
335
+ trunc_title.empty? ? words[0] : trunc_title.join(' ')
336
+ end
337
+
338
+ ##
339
+ ## Test an AppleScript response, substituting nil for
340
+ ## 'Missing Value'
341
+ ##
342
+ ## @return [Nil, String] nil if string is
343
+ ## "missing value"
344
+ ##
345
+ def nil_if_missing
346
+ return nil if self =~ /missing value/
347
+
348
+ self
349
+ end
350
+
351
+ ##
352
+ ## Score string based on number of matches, 0 - 10
353
+ ##
354
+ ## @param terms [String] The terms to
355
+ ## match
356
+ ## @param separator [String] The word separator
357
+ ## @param start_word [Boolean] Require match to be
358
+ ## at beginning of word
359
+ ##
360
+ def matches_score(terms, separator: ' ', start_word: true)
361
+ matched = 0
362
+ regexes = terms.to_rx_array(separator: separator, start_word: start_word)
363
+
364
+ regexes.each do |rx|
365
+ matched += 1 if self =~ rx
366
+ end
367
+
368
+ return 0 if matched.zero?
369
+
370
+ ((matched / regexes.count.to_f) * 10).round(3)
371
+ end
372
+
373
+ def matches_fuzzy(terms, separator: ' ', start_word: true, threshhold: 5)
374
+ sources = split(/(#{separator})+/)
375
+ words = terms.split(/(#{separator})+/)
376
+ matches = 0
377
+ sources.each do |src|
378
+ words.each do |term|
379
+ d = src.distance(term)
380
+ matches += 1 if d <= threshhold
381
+ end
382
+ end
383
+
384
+ ((matches / words.count.to_f) * 10).round(3)
385
+ end
386
+
387
+ def distance(t)
388
+ s = self.dup
389
+ m = s.length
390
+ n = t.length
391
+ return m if n == 0
392
+ return n if m == 0
393
+ d = Array.new(m+1) {Array.new(n+1)}
394
+
395
+ (0..m).each {|i| d[i][0] = i}
396
+ (0..n).each {|j| d[0][j] = j}
397
+ (1..n).each do |j|
398
+ (1..m).each do |i|
399
+ d[i][j] = if s[i-1] == t[j-1] # adjust index into string
400
+ d[i-1][j-1] # no operation required
401
+ else
402
+ [ d[i-1][j]+1, # deletion
403
+ d[i][j-1]+1, # insertion
404
+ d[i-1][j-1]+1, # substitution
405
+ ].min
406
+ end
407
+ end
408
+ end
409
+ d[m][n]
410
+ end
411
+
412
+ ##
413
+ ## Test if self contains exactl match for string (case insensitive)
414
+ ##
415
+ ## @param string [String] The string to match
416
+ ##
417
+ def matches_exact(string)
418
+ comp = gsub(/[^a-z0-9 ]/i, '')
419
+ comp =~ /\b#{string.gsub(/[^a-z0-9 ]/i, '').split(/ +/).map { |s| Regexp.escape(s) }.join(' +')}/i
420
+ end
421
+
422
+ ##
423
+ ## Test that self does not contain any of terms
424
+ ##
425
+ ## @param terms [String] The terms to test
426
+ ##
427
+ def matches_none(terms)
428
+ rx_terms = terms.is_a?(String) ? terms.to_rx_array : terms
429
+ rx_terms.each { |rx| return false if gsub(/[^a-z0-9 ]/i, '') =~ rx }
430
+ true
431
+ end
432
+
433
+ ##
434
+ ## Test if self contains any of terms
435
+ ##
436
+ ## @param terms [String] The terms to test
437
+ ##
438
+ def matches_any(terms)
439
+ rx_terms = terms.is_a?(String) ? terms.to_rx_array : terms
440
+ rx_terms.each { |rx| return true if gsub(/[^a-z0-9 ]/i, '') =~ rx }
441
+ false
442
+ end
443
+
444
+ ##
445
+ ## Test that self matches every word in terms
446
+ ##
447
+ ## @param terms [String] The terms to test
448
+ ##
449
+ def matches_all(terms)
450
+ rx_terms = terms.is_a?(String) ? terms.to_rx_array : terms
451
+ rx_terms.each { |rx| return false unless gsub(/[^a-z0-9 ]/i, '') =~ rx }
452
+ true
453
+ end
454
+
455
+ ##
456
+ ## Break a string into an array of Regexps
457
+ ##
458
+ ## @param separator [String] The word separator
459
+ ## @param start_word [Boolean] Require matches at
460
+ ## start of word
461
+ ##
462
+ ## @return [Array] array of regular expressions
463
+ ##
464
+ def to_rx_array(separator: ' ', start_word: true)
465
+ bound = start_word ? '\b' : ''
466
+ str = gsub(/(#{separator})+/, separator)
467
+ str.split(/#{separator}/).map { |arg| /#{bound}#{arg.gsub(/[^a-z0-9]/i, '.?')}/i }
468
+ end
469
+ end