Class: SL::SearchLink

Inherits:
Object
  • Object
show all
Includes:
Plist
Defined in:
lib/searchlink/config.rb,
lib/searchlink/help.rb,
lib/searchlink/parse.rb,
lib/searchlink/search.rb

Overview

Main SearchLink class

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Plist

parse_xml

Constructor Details

#initialize(opt = {}) ⇒ SearchLink

Returns a new instance of SearchLink.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/searchlink/config.rb', line 31

def initialize(opt = {})
  SL.printout = opt[:echo] || false
  unless File.exist? config_file
    default_config = <<~ENDCONFIG
      # set to true to have an HTML comment included detailing any errors
      # Can be disabled per search with `--d`, or enabled with `++d`.
      debug: true
      # set to true to have an HTML comment included reporting results
      report: true

      # use Notification Center to display progress
      notifications: false

      # when running on a file, back up original to *.bak
      backup: true

      # Time limit for searches. Increase if your searches are regularly
      # timing out
      timeout: 15

      # change this to set a specific country for search (default US)
      country_code: US

      # set to true to force inline Markdown links. Can be disabled
      # per search with `--i`, or enabled with `++i`
      inline: false

      # set to true to include a random string in reference titles.
      # Avoids conflicts if you're only running on part of a document
      # or using SearchLink multiple times within a document
      prefix_random: true

      # set to true to add titles to links based on the page title
      # of the search result. Can be disabled per search with `--t`,
      # or enabled with `++t`.
      include_titles: false

      # set to true to attempt to remove SEO elements from page titles,
      # such that "Regular expressions for beginners | Brett Terpstra.com"
      # becomes "Regular expressions for beginners"
      remove_seo: false

      # confirm existence (200) of generated links. Can be disabled
      # per search with `--v`, or enabled with `++v`.
      validate_links: false

      # If the link text is left empty, always insert the page title
      # E.g. [](!g Search Text)
      empty_uses_page_title: false

      # Formatting for social links, use %service%, %user%, and %url%
      # E.g. "%user% on %service%" => "ttscoff on Twitter"
      #      "%service%/%user%" => "Twitter/ttscoff"
      #      "%url%" => "twitter.com/ttscoff"
      social_template: "%service%/%user%"

      # append affiliate link info to iTunes urls, empty quotes for none
      # example:
      # itunes_affiliate: "&at=10l4tL&ct=searchlink"
      itunes_affiliate: "&at=10l4tL&ct=searchlink"

      # to create Amazon affiliate links, set amazon_partner to your amazon
      # affiliate tag
      #    amazon_partner: "bretttercom-20"
      amazon_partner: "bretttercom-20"

      # To create custom abbreviations for Google Site Searches,
      # add to (or replace) the hash below.
      # "abbreviation" => "site.url",
      # This allows you, for example to use [search term](!bt)
      # as a shortcut to search brettterpstra.com (using a site-specific
      # Google search). Keys in this list can override existing
      # search trigger abbreviations.
      #
      # If a custom search starts with "http" or "/", it becomes
      # a simple replacement. Any instance of "$term" is replaced
      # with a URL-escaped version of your search terms.
      # Use $term1, $term2, etc. to replace in sequence from
      # multiple search terms. No instances of "$term" functions
      # as a simple shortcut. "$term" followed by a "d" lowercases
      # the replacement. Use "$term1d," "$term2d" to downcase
      # sequential replacements (affected individually).
      # Long flags (e.g. --no-validate_links) can be used after
      # any url in the custom searches.
      #
      # Use $terms to slugify all search terms, turning
      # "Markdown Service Tools" into "markdown-service-tools"
      custom_site_searches:
        bt: brettterpstra.com
        btt: https://brettterpstra.com/topic/$term1d
        bts: /search/$term --no-validate_links
        md: www.macdrifter.com
        ms: macstories.net
        dd: www.leancrew.com
        spark: macsparky.com
        man: http://man.cx/$term
        dev: developer.apple.com
        nq: http://nerdquery.com/?media_only=0&query=$term&search=1&category=-1&catid=&type=and&results=50&db=0&prefix=0
        gs: http://scholar.google.com/scholar?btnI&hl=en&q=$term&btnG=&as_sdt=80006
      # Remove or comment (with #) history searches you don't want
      # performed by `!h`. You can force-enable them per search, e.g.
      # `!hsh` (Safari History only), `!hcb` (Chrome Bookmarks only),
      # etc. Multiple types can be strung together: !hshcb (Safari
      # History and Chrome bookmarks).
      history_types:
      - safari_bookmarks
      - safari_history
      # - chrome_history
      # - chrome_bookmarks
      # - firefox_bookmarks
      # - firefox_history
      # - edge_bookmarks
      # - edge_history
      # - brave_bookmarks
      # - brave_history
      # - arc_history
      # - arc_bookmarks
      # Pinboard search
      # You can find your api key here: https://pinboard.in/settings/password
      pinboard_api_key: ''
      # Generate an access token at https://app.bitly.com/settings/api/
      bitly_access_token: ''
      bitly_domain: 'bit.ly'
      # Custom Google API key to use Google search (free for 100 queries/day)
      google_api_key: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

    ENDCONFIG

    File.open(config_file, 'w') do |f|
      f.puts default_config
    end
  end

  config = YAML.load_file(config_file)

  # set to true to have an HTML comment inserted showing any errors
  config['debug'] ||= false

  # set to true to get a verbose report at the end of multi-line processing
  config['report'] ||= false

  config['backup'] = true unless config.key? 'backup'

  config['timeout'] ||= 15

  # set to true to force inline links
  config['inline'] ||= false

  # set to true to add titles to links based on site title
  config['include_titles'] ||= false

  # set to true to remove SEO elements from page titles
  config['remove_seo'] ||= false

  # set to true to use page title as link text when empty
  config['empty_uses_page_title'] ||= false

  # change this to set a specific country for search (default US)
  config['country_code'] ||= 'US'

  # set to true to include a random string in ref titles
  # allows running SearchLink multiple times w/out conflicts
  config['prefix_random'] = false unless config['prefix_random']

  config['social_template'] ||= '%service%/%user%'

  # append affiliate link info to iTunes urls, empty quotes for none
  # example:
  # $itunes_affiliate = "&at=10l4tL&ct=searchlink"
  config['itunes_affiliate'] ||= '&at=10l4tL&ct=searchlink'

  # to create Amazon affiliate links, set amazon_partner to your amazon
  # affiliate tag
  #    amazon_partner: "bretttercom-20"
  config['amazon_partner'] ||= ''

  # To create custom abbreviations for Google Site Searches,
  # add to (or replace) the hash below.
  # "abbreviation" => "site.url",
  # This allows you, for example to use [search term](!bt)
  # as a shortcut to search brettterpstra.com. Keys in this
  # hash can override existing search triggers.
  config['custom_site_searches'] ||= {
    'bt' => 'brettterpstra.com',
    'imdb' => 'imdb.com'
  }

  # confirm existence of links generated from custom search replacements
  config['validate_links'] ||= false

  # use notification center to show progress
  config['notifications'] ||= false
  config['pinboard_api_key'] ||= false
  config['google_api_key'] ||= false

  SL.line_num = nil
  SL.match_column = nil
  SL.match_length = nil
  SL.config = config
end

Instance Attribute Details

#clipboardObject (readonly)

Returns the value of attribute clipboard.



8
9
10
# File 'lib/searchlink/search.rb', line 8

def clipboard
  @clipboard
end

#originputObject (readonly)

Returns the value of attribute originput.



8
9
10
# File 'lib/searchlink/search.rb', line 8

def originput
  @originput
end

#outputObject (readonly)

Returns the value of attribute output.



8
9
10
# File 'lib/searchlink/search.rb', line 8

def output
  @output
end

Instance Method Details

#config_fileObject

Values found in ~/.searchlink will override defaults in this script



20
21
22
23
24
25
26
27
28
29
# File 'lib/searchlink/config.rb', line 20

def config_file
  old_style = File.expand_path('~/.searchlink')
  new_style = File.expand_path('~/.config/searchlink/config.yaml')
  if File.exist?(old_style) && !File.exist?(new_style)
    old_style
  else
    FileUtils.mkdir_p(File.dirname(new_style))
    new_style
  end
end

#help_cliObject



99
100
101
# File 'lib/searchlink/help.rb', line 99

def help_cli
  $stdout.puts help_text
end

#help_cssObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/searchlink/help.rb', line 3

def help_css
  <<~ENDCSS
    body{-webkit-font-smoothing:antialiased;font-family:"Avenir Next",Avenir,"Helvetica Neue",Helvetica,Arial,Verdana,sans-serif;
    margin:30px 0 0;padding:0;background:#fff;color:#303030;font-size:16px;line-height:1.5;text-align:center}h1{color:#000}
    h2{color:#111}p,td,div{color:#111;font-family:"Avenir Next",Avenir,"Helvetica Neue",Helvetica,Arial,Verdana,sans-serif;
    word-wrap:break-word}a{color:#de5456;text-decoration:none;-webkit-transition:color .2s ease-in-out;
    -moz-transition:color .2s ease-in-out;-o-transition:color .2s ease-in-out;-ms-transition:color .2s ease-in-out;
    transition:color .2s ease-in-out}a:hover{color:#3593d9}h1,h2,h3,h4,h5{margin:2.75rem 0 2rem;font-weight:500;line-height:1.15}
    h1{margin-top:0;font-size:2em}h2{font-size:1.7em}ul,ol,pre,table,blockquote{margin-top:2em;margin-bottom:2em}
    caption,col,colgroup,table,tbody,td,tfoot,th,thead,tr{border-spacing:0}table{border:1px solid rgba(0,0,0,0.25);
    border-collapse:collapse;display:table;empty-cells:hide;margin:-1px 0 1.3125em;padding:0;table-layout:fixed;margin:0 auto}
    caption{display:table-caption;font-weight:700}col{display:table-column}colgroup{display:table-column-group}
    tbody{display:table-row-group}tfoot{display:table-footer-group}thead{display:table-header-group}
    td,th{display:table-cell}tr{display:table-row}table th,table td{font-size:1.2em;line-height:1.3;padding:.5em 1em 0}
    table thead{background:rgba(0,0,0,0.15);border:1px solid rgba(0,0,0,0.15);border-bottom:1px solid rgba(0,0,0,0.2)}
    table tbody{background:rgba(0,0,0,0.05)}table tfoot{background:rgba(0,0,0,0.15);border:1px solid rgba(0,0,0,0.15);
    border-top:1px solid rgba(0,0,0,0.2)}p{font-size:1.1429em;line-height:1.72em;margin:1.3125em 0}dt,th{font-weight:700}
    table tr:nth-child(odd),table th:nth-child(odd),table td:nth-child(odd){background:rgba(255,255,255,0.06)}
    table tr:nth-child(even),table td:nth-child(even){background:rgba(200,200,200,0.25)}
    input[type=text] {padding: 5px;border-radius: 5px;border: solid 1px #ccc;font-size: 20px;}
  ENDCSS
end

#help_dialogObject



87
88
89
90
91
92
93
94
95
96
97
# File 'lib/searchlink/help.rb', line 87

def help_dialog
  text = ["<html><head><style>#{help_css}</style><script>#{help_js}</script></head><body>"]
  text << '<h1>SearchLink Help</h1>'
  text << "<p>[#{SL.version_check}] [<a href='https://github.com/ttscoff/searchlink/wiki'>Wiki</a>]</p>"
  text << help_html
  text << '<p><a href="https://github.com/ttscoff/searchlink/wiki">Visit the wiki</a> for additional information</p>'
  text << '</body>'
  html_file = File.expand_path('~/.searchlink_searches.html')
  File.open(html_file, 'w') { |f| f.puts text.join("\n") }
  `open #{html_file}`
end

#help_htmlObject



73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/searchlink/help.rb', line 73

def help_html
  out = ['<input type="text" id="filter" onkeyup="filterTable()" placeholder="Filter searches">']
  out << '<h2>Available Searches</h2>'
  out << SL::Searches.available_searches_html
  out << '<h2>Custom Searches</h2>'
  out << '<table id="custom">'
  out << '<thead><td>Shortcut</td><td>Search Type</td></thead>'
  out << '<tbody>'
  SL.config['custom_site_searches'].each { |label, site| out << "<tr><td><code>!#{label}</code></td><td>#{site}</td></tr>" }
  out << '</tbody>'
  out << '</table>'
  out.join("\n")
end

#help_jsObject



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/searchlink/help.rb', line 26

def help_js
  <<~EOJS
    function filterTable() {
      let input, filter, table, tr, i, txtValue;
      input = document.getElementById("filter");
      filter = input.value.toUpperCase();
      table = document.getElementById("searches");
      table2 = document.getElementById("custom");

      tr = table.getElementsByTagName("tr");

      for (i = 0; i < tr.length; i++) {
          txtValue = tr[i].textContent || tr[i].innerText;
          if (txtValue.toUpperCase().indexOf(filter) > -1) {
            tr[i].style.display = "";
          } else {
            tr[i].style.display = "none";
          }
      }

      tr = table2.getElementsByTagName("tr");

      for (i = 0; i < tr.length; i++) {
          txtValue = tr[i].textContent || tr[i].innerText;
          if (txtValue.toUpperCase().indexOf(filter) > -1) {
            tr[i].style.display = "";
          } else {
            tr[i].style.display = "none";
          }
      }
    }
  EOJS
end

#help_textObject



60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/searchlink/help.rb', line 60

def help_text
  text = <<~EOHELP
    -- [Available searches] -------------------
    #{SL::Searches.available_searches}
  EOHELP

  if SL.config['custom_site_searches']
    text += "\n-- [Custom Searches] ----------------------\n"
    SL.config['custom_site_searches'].sort_by { |l, s| l }.each { |label, site| text += "!#{label}#{label.spacer} #{site}\n" }
  end
  text
end

#parse(input) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/searchlink/parse.rb', line 76

def parse(input)
  SL.output = []
  return false if input.empty?

  parse_arguments(input, { only_meta: true })
  SL.originput = input.dup

  parse_commands(input)

  SL.config['inline'] = true if input.scan(/\]\(/).length == 1 && input.split(/\n/).length == 1
  SL.errors = {}
  SL.report = []

  # Check for new version
  latest_version = SL.new_version?
  if latest_version
    SL.add_output("<!-- v#{latest_version} available, run SearchLink on the word 'update' to install. -->")
  end

  links = {}
  SL.footer = []
  counter_links = 0
  counter_errors = 0

  input.sub!(/\n?<!-- Report:.*?-->\n?/m, '')
  input.sub!(/\n?<!-- Errors:.*?-->\n?/m, '')

  input.scan(/\[(.*?)\]:\s+(.*?)\n/).each { |match| links[match[1].strip] = match[0] }

  prefix = if SL.config['prefix_random']
             if input =~ /\[(\d{4}-)\d+\]: \S+/
               Regexp.last_match(1)
             else
               format('%04d-', rand(9999))
             end
           else
             ''
           end

  highest_marker = 0
  input.scan(/^\s{,3}\[(?:#{prefix})?(\d+)\]: /).each do
    m = Regexp.last_match
    highest_marker = m[1].to_i if m[1].to_i > highest_marker
  end

  footnote_counter = 0
  input.scan(/^\s{,3}\[\^(?:#{prefix})?fn(\d+)\]: /).each do
    m = Regexp.last_match
    footnote_counter = m[1].to_i if m[1].to_i > footnote_counter
  end

  if input =~ /\[(.*?)\]\((.*?)\)/
    lines = input.split(/\n/)
    out = []

    total_links = input.scan(/\[(.*?)\]\((.*?)\)/).length
    in_code_block = false
    line_difference = 0
    lines.each_with_index do |line, num|
      SL.line_num = num - line_difference
      cursor_difference = 0
      # ignore links in code blocks
      if line =~ /^( {4,}|\t+)[^*+\-]/
        out.push(line)
        next
      end
      if line =~ /^[~`]{3,}/
        if in_code_block
          in_code_block = false
          out.push(line)
          next
        else
          in_code_block = true
        end
      end
      if in_code_block
        out.push(line)
        next
      end

      delete_line = false

      search_count = 0

      line.gsub!(/\[(.*?)\]\((.*?)\)/) do |match|
        this_match = Regexp.last_match
        SL.match_column = this_match.begin(0) - cursor_difference
        match_string = this_match.to_s
        SL.match_length = match_string.length
        match_before = this_match.pre_match

        invalid_search = false
        ref_title = false

        if match_before.scan(/(^|[^\\])`/).length.odd?
          SL.add_report("Match '#{match_string}' within an inline code block")
          invalid_search = true
        end

        counter_links += 1
        unless SILENT
          $stderr.print("\033[0K\rProcessed: #{counter_links} of #{total_links}, #{counter_errors} errors. ")
        end

        link_text = this_match[1] || ''
        link_info = parse_arguments(this_match[2].strip).strip || ''

        if link_text.strip == '' && link_info =~ /".*?"/
          link_info.gsub!(/"(.*?)"/) do
            m = Regexp.last_match(1)
            link_text = m if link_text == ''
            %("#")
          end
        end

        link_info.gsub!(/<(.*?)>/) do
          %(%22#{Regexp.last_match(1)}%22)
        end

        if link_info.strip =~ /:$/ && line.strip == match
          ref_title = true
          link_info.sub!(/\s*:\s*$/, '')
        end

        unless !link_text.empty? || !link_info.sub(/^[!\^]\S+/, '').strip.empty?
          SL.add_error('No input', match)
          counter_errors += 1
          invalid_search = true
        end

        if link_info =~ /^!(\S+)/
          search_type = Regexp.last_match(1)
          unless SL::Searches.valid_search?(search_type) || search_type =~ /^(\S+\.)+\S+$/
            SL.add_error("Invalid search#{SL::Searches.did_you_mean(search_type)}", match)
            invalid_search = true
          end
        end

        if invalid_search
          match
        elsif link_info =~ /^\^(.+)/
          m = Regexp.last_match
          if m[1].nil? || m[1] == ''
            match
          else
            note = m[1].strip
            footnote_counter += 1
            ref = if !link_text.empty? && link_text.scan(/\s/).empty?
                    link_text
                  else
                    format('%<p>sfn%<c>04d', p: prefix, c: footnote_counter)
                  end
            SL.add_footer "[^#{ref}]: #{note}"
            res = "[^#{ref}]"
            cursor_difference += (SL.match_length - res.length)
            SL.match_length = res.length
            SL.add_report("#{match_string} => Footnote #{ref}")
            res
          end
        # Handle [](URL) and [%](URL), filling in title
        elsif (link_text == '' || link_text == '%') && SL::URL.url?(link_info)
          url = link_info
          title = SL::URL.title(link_info)
          link_text = title

          if ref_title
            unless links.key? url
              links[url] = link_text
              SL.add_footer SL.make_link(:ref_title, link_text, url, title: title, force_title: false)
            end
            delete_line = true
          elsif SL.config['inline']
            res = SL.make_link(:inline, link_text, url, title: title, force_title: false)
            cursor_difference += SL.match_length - res.length
            SL.match_length = res.length
            SL.add_report("#{match_string} => #{url}")
            res
          else
            unless links.key? url
              highest_marker += 1
              links[url] = format('%<pre>s%<m>04d', pre: prefix, m: highest_marker)
              SL.add_footer SL.make_link(:ref_title, links[url], url, title: title, force_title: false)
            end

            type = SL.config['inline'] ? :inline : :ref_link
            res = SL.make_link(type, link_text, links[url], title: false, force_title: false)
            cursor_difference += SL.match_length - res.length
            SL.match_length = res.length
            SL.add_report("#{match_string} => #{url}")
            res
          end
        elsif (link_text == '' && link_info == '') || SL::URL.url?(link_info)
          SL.add_error('Invalid search', match) unless SL::URL.url?(link_info)
          match
        else
          link_info = link_text if !link_text.empty? && link_info == ''

          search_type = ''
          search_terms = ''
          link_only = false
          SL.clipboard = false
          SL.titleize = SL.config['empty_uses_page_title']

          if link_info =~ /^(?:[!\^](\S+))\s*(.*)$/
            m = Regexp.last_match

            search_type = m[1].nil? ? (SL::GoogleSearch.test_for_key ? 'gg' : 'g') : m[1]

            search_terms = m[2].gsub(/(^["']|["']$)/, '')
            search_terms.strip!

            # if the link text is just '%' replace with title regardless of config settings
            if link_text == '%' && search_terms && !search_terms.empty?
              SL.titleize = true
              link_text = ''
            end

            search_terms = link_text if search_terms == ''

            # if the input starts with a +, append it to the link text as the search terms
            search_terms = "#{link_text} #{search_terms.strip.sub(/^\+\s*/, '')}" if search_terms.strip =~ /^\+[^+]/

            # if the end of input contain "^", copy to clipboard instead of STDOUT
            SL.clipboard = true if search_terms =~ /(!!)?\^(!!)?$/

            # if the end of input contains "!!", only print the url
            link_only = true if search_terms =~ /!!\^?$/

            search_terms.sub!(/(!!)?\^?(!!)?$/,"")

          elsif link_info =~ /^!/
            search_word = link_info.match(/^!(\S+)/)

            if search_word && SL::Searches.valid_search?(search_word[1])
              search_type = search_word[1] unless search_word.nil?
              search_terms = link_text
            elsif search_word && search_word[1] =~ /^(\S+\.)+\S+$/
              search_type = SL::GoogleSearch.test_for_key ? 'gg' : 'g'
              puts SL::GoogleSearch.test_for_key
              search_terms = "site:#{search_word[1]} #{link_text}"
            else
              SL.add_error("Invalid search#{SL::Searches.did_you_mean(search_word[1])}", match)
              search_type = false
              search_terms = false
            end
          elsif link_text && !link_text.empty? && (!link_info || link_info.empty?)
            search_type = SL::GoogleSearch.test_for_key ? 'gg' : 'g'
            search_terms = link_text
          elsif link_info && !link_info.empty?
            search_type = SL::GoogleSearch.test_for_key ? 'gg' : 'g'
            search_terms = link_info
          else
            SL.add_error('Invalid search', match)
            search_type = false
            search_terms = false
          end

          if search_type && !search_terms.empty?
            SL.config['custom_site_searches'].each do |k, v|
              next unless search_type == k

              link_text = search_terms if !SL.titleize && link_text == ''
              v = parse_arguments(v, { no_restore: true })
              if v =~ %r{^(/|http)}i
                search_type = 'r'
                tokens = v.scan(/\$term\d+[ds]?/).sort.uniq

                if !tokens.empty?
                  highest_token = 0
                  tokens.each do |token|
                    if token =~ /(\d+)[ds]?$/ && Regexp.last_match(1).to_i > highest_token
                      highest_token = Regexp.last_match(1).to_i
                    end
                  end
                  terms_p = search_terms.split(/ +/)
                  if terms_p.length > highest_token
                    remainder = terms_p[highest_token - 1..-1].join(' ')
                    terms_p = terms_p[0..highest_token - 2]
                    terms_p.push(remainder)
                  end
                  tokens.each do |t|
                    next unless t =~ /(\d+)[ds]?$/

                    int = Regexp.last_match(1).to_i - 1
                    replacement = terms_p[int]
                    case t
                    when /d$/
                      replacement.downcase!
                      re_down = ''
                    when /s$/
                      replacement.slugify!
                      re_down = ''
                    else
                      re_down = '(?!d|s)'
                    end
                    v.gsub!(/#{Regexp.escape(t) + re_down}/, replacement.url_encode)
                  end
                  search_terms = v
                else
                  search_terms = v.gsub(/\$term[ds]?/i) do |mtch|
                    search_terms.downcase! if mtch =~ /d$/i
                    search_terms.slugify! if mtch =~ /s$/i
                    search_terms.url_encode
                  end
                end
              else
                search_type = SL::GoogleSearch.test_for_key ? 'gg' : 'g'
                search_terms = "site:#{v} #{search_terms}"
              end

              break
            end
          end

          if (search_type && search_terms) || url
            # warn "Searching #{search_type} for #{search_terms}"
            if (!url)
              search_count += 1
              url, title, link_text = do_search(search_type, search_terms, link_text, search_count)
            end

            if url
              title = SL::URL.title(url) if SL.titleize && title == ''

              link_text = title if link_text == '' && title
              force_title = search_type =~ /def/ ? true : false

              if link_only || search_type =~ /sp(ell)?/ || url == 'embed'
                url = title if url == 'embed'
                cursor_difference += SL.match_length - url.length
                SL.match_length = url.length
                SL.add_report("#{match_string} => #{url}")
                url
              elsif ref_title
                unless links.key? url
                  links[url] = link_text
                  SL.add_footer SL.make_link(:ref_title, link_text, url, title: title, force_title: force_title)
                end
                delete_line = true
              elsif SL.config['inline']
                res = SL.make_link(:inline, link_text, url, title: title, force_title: force_title)
                cursor_difference += SL.match_length - res.length
                SL.match_length = res.length
                SL.add_report("#{match_string} => #{url}")
                res
              else
                unless links.key? url
                  highest_marker += 1
                  links[url] = format('%<pre>s%<m>04d', pre: prefix, m: highest_marker)
                  SL.add_footer SL.make_link(:ref_title, links[url], url, title: title, force_title: force_title)
                end

                type = SL.config['inline'] ? :inline : :ref_link
                res = SL.make_link(type, link_text, links[url], title: false, force_title: force_title)
                cursor_difference += SL.match_length - res.length
                SL.match_length = res.length
                SL.add_report("#{match_string} => #{url}")
                res
              end
            else
               SL.add_error('No results', "#{search_terms} (#{match_string})")
              counter_errors += 1
              match
            end
          else
            SL.add_error('Invalid search', match)
            counter_errors += 1
            match
          end
        end
      end
      line_difference += 1 if delete_line
      out.push(line) unless delete_line
      delete_line = false
    end
    warn "\n" unless SILENT

    input = out.delete_if { |l| l.strip =~ /^<!--DELETE-->$/ }.join("\n")

    if SL.config['inline']
      SL.add_output "#{input}\n"
      SL.add_output "\n#{SL.print_footer}" unless SL.footer.empty?
    elsif SL.footer.empty?
      SL.add_output input
    else
      last_line = input.strip.split(/\n/)[-1]
      case last_line
      when /^\[.*?\]: http/
        SL.add_output "#{input.rstrip}\n"
      when /^\[\^.*?\]: /
        SL.add_output input.rstrip
      else
        SL.add_output "#{input}\n\n"
      end
      SL.add_output "#{SL.print_footer}\n\n"
    end

    SL.line_num = nil
    SL.add_report("Processed: #{total_links} links, #{counter_errors} errors.")
    SL.print_report
    SL.print_errors
  else
    link_only = false
    SL.clipboard = false

    res = parse_arguments(input.strip!).strip
    input = res.nil? ? input.strip : res

    # if the end of input contain "^", copy to clipboard instead of STDOUT
    SL.clipboard = true if input =~ /\^[!~:\s]*$/

    # if the end of input contains "!!", only print the url
    link_only = true if input =~ /!![\^~:\s]*$/

    reference_link = input =~ /:([!\^~\s]*)$/

    # if end of input contains ~, pull url from clipboard
    if input =~ /~[:\^!\s]*$/
      input.sub!(/[:!\^\s~]*$/, '')
      clipboard = `__CF_USER_TEXT_ENCODING=$UID:0x8000100:0x8000100 pbpaste`.strip
      if SL::URL.url?(clipboard)
        type = reference_link ? :ref_title : :inline
        print SL.make_link(type, input.strip, clipboard)
      else
        print SL.originput
      end
      Process.exit
    end

    input.sub!(/[:!\^\s~]*$/, '')

    ## Maybe if input is just a URL, convert it to a link
    ## using hostname as text without doing search
    if SL::URL.only_url?(input.strip)
      type = reference_link ? :ref_title : :inline
      url, title = SL::URL.url_to_link(input.strip, type)
      print SL.make_link(type, title, url, title: false, force_title: false)
      Process.exit
    end

    # check for additional search terms in parenthesis
    additional_terms = ''
    if input =~ /\((.*?)\)/
      additional_terms = " #{Regexp.last_match(1).strip}"
      input.sub!(/\(.*?\)/, '')
    end

    # Maybe detect "search + addition terms" and remove additional terms from link text?
    # if input =~ /\+(.+?)$/
    #   additional_terms = "#{additional_terms} #{Regexp.last_match(1).strip}"
    #   input.sub!(/\+.*?$/, '').strip!
    # end

    link_text = false

    if input =~ /"(.*?)"/
      link_text = Regexp.last_match(1)
      input.gsub!(/"(.*?)"/, '\1')
    end

    # remove quotes from terms, just in case
    # input.sub!(/^(!\S+)?\s*(["'])(.*?)\2([\!\^]+)?$/, "\\1 \\3\\4")

    case input
    when /^!(\S+)\s+(.*)$/
      type = Regexp.last_match(1)
      link_info = Regexp.last_match(2).strip
      link_text ||= link_info
      terms = link_info + additional_terms
      terms.strip!

      if SL::Searches.valid_search?(type) || type =~ /^(\S+\.)+\S+$/
        if type && terms && !terms.empty?
          # Iterate through custom searches for a match, perform search if matched
          SL.config['custom_site_searches'].each do |k, v|
            next unless type == k

            link_text = terms if link_text == ''
            v = parse_arguments(v, { no_restore: true })
            if v =~ %r{^(/|http)}i
              type = 'r'
              tokens = v.scan(/\$term\d+[ds]?/).sort.uniq

              if !tokens.empty?
                highest_token = 0
                tokens.each do |token|
                  t = Regexp.last_match(1)
                  highest_token = t.to_i if token =~ /(\d+)d?$/ && t.to_i > highest_token
                end
                terms_p = terms.split(/ +/)
                if terms_p.length > highest_token
                  remainder = terms_p[highest_token - 1..].join(' ')
                  terms_p = terms_p[0..highest_token - 2]
                  terms_p.push(remainder)
                end
                tokens.each do |t|
                  next unless t =~ /(\d+)d?$/

                  int = Regexp.last_match(1).to_i - 1
                  replacement = terms_p[int]

                  re_down = case t
                            when /d$/
                              replacement.downcase!
                              ''
                            when /s$/
                              replacement.slugify!
                              ''
                            else
                              '(?!d|s)'
                            end
                  v.gsub!(/#{Regexp.escape(t) + re_down}/, replacement.url_encode)
                end
                terms = v
              else
                terms = v.gsub(/\$term[ds]?/i) do |mtch|
                  terms.downcase! if mtch =~ /d$/i
                  terms.slugify! if mtch =~ /s$/i
                  terms.url_encode
                end
              end
            else
              type = SL::GoogleSearch.test_for_key ? 'gg' : 'g'
              terms = "site:#{v} #{terms}"
            end

            break
          end
        end

        # if contains TLD, use site-specific search
        if type =~ /^(\S+\.)+\S+$/
          terms = "site:#{type} #{terms}"
          type = SL::GoogleSearch.test_for_key ? 'gg' : 'g'
        end
        search_count ||= 0
        search_count += 1

        url, title, link_text = do_search(type, terms, link_text, search_count)
      else
        SL.add_error("Invalid search#{SL::Searches.did_you_mean(type)}", input)
        counter_errors += 1
      end
    # Social handle expansion
    when /^([tfilm])?@(\S+)\s*$/
      type = Regexp.last_match(1)
      unless type
        # If contains @ mid-handle, use Mastodon
        if Regexp.last_match(2) =~ /[a-z0-9_]@[a-z0-9_.]+/i
          type = 'm'
        else
          type = 't'
        end
      end
      link_text = input.sub(/^[tfilm]/, '')
      url, title = SL::SocialSearch.social_handle(type, link_text)
      link_text = title
    else
      link_text ||= input
      url, title, link_text = SL.ddg(input, link_text)
    end

    if url
      if type =~ /sp(ell)?/
        SL.add_output(url)
      elsif link_only
        SL.add_output(url)
      elsif url == 'embed'
        SL.add_output(title)
      else
        type = reference_link ? :ref_title : :inline

        SL.add_output SL.make_link(type, link_text, url, title: title, force_title: false)
        SL.print_errors
      end
    else
      SL.add_error('No results', title)
      SL.add_output SL.originput.chomp
      SL.print_errors
    end

    if SL.clipboard
      if SL.output == SL.originput
        warn 'No results found'
      else
        `echo #{Shellwords.escape(SL.output.join(''))}|tr -d "\n"|pbcopy`
        warn 'Results in clipboard'
      end
    end
  end
end

#parse_arguments(string, opt = {}) ⇒ String

Parse arguments in the input string

Parameters:

  • string (String)

    the string to parse

  • opt (Hash) (defaults to: {})

    the options to parse

Options Hash (opt):

  • :only_meta (Boolean) — default: false

    whether to skip flags

  • :no_restore (Boolean) — default: false

    whether to restore previous config

Returns:

  • (String)

    the parsed string



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/searchlink/parse.rb', line 11

def parse_arguments(string, opt={})
  input = string.dup
  return "" if input.nil?

  skip_flags = opt[:only_meta] || false
  no_restore = opt[:no_restore] || false
  restore_prev_config unless no_restore

  input.parse_flags! unless skip_flags

  options = %w[debug country_code inline prefix_random include_titles remove_seo validate_links]
  options.each do |o|
    if input =~ /^ *#{o}:\s+(\S+)$/
      val = Regexp.last_match(1).strip
      val = true if val =~ /true/i
      val = false if val =~ /false/i
      SL.config[o] = val
      $stderr.print "\r\033[0KGlobal config: #{o} = #{SL.config[o]}\n" unless SILENT
    end

    next if skip_flags

    while input =~ /^#{o}:\s+(.*?)$/ || input =~ /--(no-)?#{o}/
      next unless input =~ /--(no-)?#{o}/ && !skip_flags

      unless SL.prev_config.key? o
        SL.prev_config[o] = SL.config[o]
        bool = Regexp.last_match(1).nil? || Regexp.last_match(1) == '' ? true : false
        SL.config[o] = bool
        $stderr.print "\r\033[0KLine config: #{o} = #{SL.config[o]}\n" unless SILENT
      end
      input.sub!(/\s?--(no-)?#{o}/, '')
    end
  end
  SL.clipboard ? string : input
end

#parse_commands(input) ⇒ Object

Parse commands from the given input string

Parameters:

  • input (String)

    the input string



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/searchlink/parse.rb', line 51

def parse_commands(input)
  # Handle commands like help or docs
  return unless input.strip =~ /^!?(h(elp)?|wiki|docs?|v(er(s(ion)?)?)?|up(date|grade))$/

  case input.strip
  when /^!?help$/i
    if SILENT
      help_dialog # %x{open http://brettterpstra.com/projects/searchlink/}
    else
      $stdout.puts SL.version_check.to_s
      $stdout.puts 'See https://github.com/ttscoff/searchlink/wiki for help'
    end
    print input
  when /^!?(wiki|docs)$/i
    warn 'Opening wiki in browser'
    `open https://github.com/ttscoff/searchlink/wiki`
  when /^!?v(er(s(ion)?)?)?$/
    print "[#{SL.version_check}]"
  when /^!?up(date|grade)$/
    SL.update_searchlink
    print SL.output.join('')
  end
  Process.exit 0
end

#restore_prev_configObject



232
233
234
235
236
237
238
# File 'lib/searchlink/config.rb', line 232

def restore_prev_config
  @prev_config&.each do |k, v|
    SL.config[k] = v
    $stderr.print "\r\033[0KReset config: #{k} = #{SL.config[k]}\n" unless SILENT
  end
  @prev_config = {}
end