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.



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
# File 'lib/searchlink/config.rb', line 33

def initialize(opt = {})
  SL.printout = opt[:echo] || false
  if File.exist? config_file
    write_new_plugin_config
  else
    default_config = "      # set to true to have an HTML comment included detailing any errors\n      # Can be disabled per search with `--d`, or enabled with `++d`.\n      debug: true\n      # set to true to have an HTML comment included reporting results\n      report: true\n\n      # use Notification Center to display progress\n      notifications: false\n\n      # when running on a file, back up original to *.bak\n      backup: true\n\n      # Time limit for searches. Increase if your searches are regularly\n      # timing out\n      timeout: 15\n\n      # change this to set a specific country for search (default US)\n      country_code: US\n\n      # set to true to force inline Markdown links. Can be disabled\n      # per search with `--i`, or enabled with `++i`\n      inline: false\n\n      # set to true to include a random string in reference titles.\n      # Avoids conflicts if you're only running on part of a document\n      # or using SearchLink multiple times within a document\n      prefix_random: true\n\n      # set to true to add titles to links based on the page title\n      # of the search result. Can be disabled per search with `--t`,\n      # or enabled with `++t`.\n      include_titles: false\n\n      # set to true to attempt to remove SEO elements from page titles,\n      # such that \"Regular expressions for beginners | Brett Terpstra.com\"\n      # becomes \"Regular expressions for beginners\"\n      remove_seo: false\n\n      # confirm existence (200) of generated links. Can be disabled\n      # per search with `--v`, or enabled with `++v`.\n      validate_links: false\n\n      # If the link text is left empty, always insert the page title\n      # E.g. [](!g Search Text)\n      empty_uses_page_title: false\n\n      # If confirm is true, then a popup dialog will be displayed\n      # showing the destination of each found link. Hitting cancel\n      # will leave the link unchanged.\n      confirm: false\n\n      # To create custom abbreviations for Google Site Searches,\n      # add to (or replace) the hash below.\n      # \"abbreviation\" => \"site.url\",\n      # This allows you, for example to use [search term](!bt)\n      # as a shortcut to search brettterpstra.com (using a site-specific\n      # Google search). Keys in this list can override existing\n      # search trigger abbreviations.\n      #\n      # If a custom search starts with \"http\" or \"/\", it becomes\n      # a simple replacement. Any instance of \"$term\" is replaced\n      # with a URL-escaped version of your search terms.\n      # Use $term1, $term2, etc. to replace in sequence from\n      # multiple search terms. No instances of \"$term\" functions\n      # as a simple shortcut. \"$term\" followed by a \"d\" lowercases\n      # the replacement. Use \"$term1d,\" \"$term2d\" to downcase\n      # sequential replacements (affected individually).\n      # Long flags (e.g. --no-validate_links) can be used after\n      # any url in the custom searches.\n      #\n      # Use $terms to slugify all search terms, turning\n      # \"Markdown Service Tools\" into \"markdown-service-tools\"\n      custom_site_searches:\n        bt: brettterpstra.com\n        btt: https://brettterpstra.com/topic/$term1d\n        bts: /search/$term --no-validate_links\n        md: www.macdrifter.com\n        ms: macstories.net\n        dd: www.leancrew.com\n        spark: macsparky.com\n        man: http://man.cx/$term\n        dev: developer.apple.com\n        nq: http://nerdquery.com/?media_only=0&query=$term&search=1&category=-1&catid=&type=and&results=50&db=0&prefix=0\n        gs: http://scholar.google.com/scholar?btnI&hl=en&q=$term&btnG=&as_sdt=80006\n\n    ENDCONFIG\n\n    default_config = get_plugin_configs(default_config)\n\n    File.open(config_file, \"w\") do |f|\n      f.puts default_config\n    end\n  end\n\n  config = YAML.load_file(config_file)\n\n  # set to true to have an HTML comment inserted showing any errors\n  config[\"debug\"] ||= false\n\n  # set to true to get a verbose report at the end of multi-line processing\n  config[\"report\"] ||= false\n\n  config[\"backup\"] = true unless config.key? \"backup\"\n\n  config[\"timeout\"] ||= 15\n\n  # set to true to force inline links\n  config[\"inline\"] ||= false\n\n  # set to true to add titles to links based on site title\n  config[\"include_titles\"] ||= false\n\n  # set to true to remove SEO elements from page titles\n  config[\"remove_seo\"] ||= false\n\n  # set to true to use page title as link text when empty\n  config[\"empty_uses_page_title\"] ||= false\n\n  # change this to set a specific country for search (default US)\n  config[\"country_code\"] ||= \"US\"\n\n  # set to true to include a random string in ref titles\n  # allows running SearchLink multiple times w/out conflicts\n  config[\"prefix_random\"] = false unless config[\"prefix_random\"]\n\n  config[\"social_template\"] ||= \"%service%/%user%\"\n\n  # append affiliate link info to iTunes urls, empty quotes for none\n  # example:\n  # $itunes_affiliate = \"&at=10l4tL&ct=searchlink\"\n  config[\"itunes_affiliate\"] ||= \"&at=10l4tL&ct=searchlink\"\n\n  # to create Amazon affiliate links, set amazon_partner to your amazon\n  # affiliate tag\n  #    amazon_partner: \"bretttercom-20\"\n  config[\"amazon_partner\"] ||= \"\"\n\n  # display a popup dialog confirmation\n  config[\"confirm\"] ||= false\n\n  # To create custom abbreviations for Google Site Searches,\n  # add to (or replace) the hash below.\n  # \"abbreviation\" => \"site.url\",\n  # This allows you, for example to use [search term](!bt)\n  # as a shortcut to search brettterpstra.com. Keys in this\n  # hash can override existing search triggers.\n  config[\"custom_site_searches\"] ||= {\n    \"bt\" => \"brettterpstra.com\",\n    \"imdb\" => \"imdb.com\",\n  }\n\n  # confirm existence of links generated from custom search replacements\n  config[\"validate_links\"] ||= false\n\n  # use notification center to show progress\n  config[\"notifications\"] ||= false\n\n  SL.line_num = nil\n  SL.match_column = nil\n  SL.match_length = nil\n  SL.config = config\n\n  add_plugin_configs(config)\nend\n"

Instance Attribute Details

#clipboardObject (readonly)

Returns the value of attribute clipboard.



10
11
12
# File 'lib/searchlink/search.rb', line 10

def clipboard
  @clipboard
end

#originputObject (readonly)

Returns the value of attribute originput.



10
11
12
# File 'lib/searchlink/search.rb', line 10

def originput
  @originput
end

#outputObject (readonly)

Returns the value of attribute output.



10
11
12
# File 'lib/searchlink/search.rb', line 10

def output
  @output
end

Instance Method Details

#add_plugin_configs(config) ⇒ Object

Note:

applies configurations to SL.config

Add plugin configurations to config object

Parameters:

  • config (Hash)

    Hash of plugin configurations



210
211
212
213
214
215
216
217
218
# File 'lib/searchlink/config.rb', line 210

def add_plugin_configs(config)
  SL::Searches.plugins[:search].each_value do |plugin|
    next unless plugin.key?(:config) && !plugin[:config].nil? && !plugin[:config].empty?

    plugin[:config].each do |cfg|
      SL.config[cfg[:key]] = config[cfg[:key]] if config.key?(cfg[:key])
    end
  end
end

#config_fileObject

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



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

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

#confirmed?(url) ⇒ Boolean

Confirm a URL with a popup if requested

Returns:

  • (Boolean)


6
7
8
9
10
# File 'lib/searchlink/parse.rb', line 6

def confirmed?(url)
  return true unless SL.config["confirm"]

  SL::Shortener.confirm?(url)
end

#get_plugin_config(cfg) ⇒ String

Get a single plugin configuration

Parameters:

  • cfg (Hash)

    Hash of single plugin config

Returns:

  • (String)

    String representation of config



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
# File 'lib/searchlink/config.rb', line 266

def get_plugin_config(cfg)
  key = cfg[:key]
  value = cfg[:value]
  required = cfg[:required]
  description = cfg[:description]
  description = "\n#{description}" if description
  description = description.word_wrap(60, "# ") if description
  key = required ? key : "# #{key}"
  if value.is_a?(Array)
    array_value = "\n"
    value.each do |v|
      array_value += required ? "- #{v.yaml_val}" : "# - #{v.yaml_val}\n"
    end
    value = array_value
  elsif value.is_a?(Hash)
    hash_value = "\n"
    value.each do |k, v|
      hash_value += required ? "  #{k}: #{v.yaml_val}" : "#  #{k}: #{v.yaml_val}"
    end
    value = hash_value
  else
    value = value.yaml_val
  end
  new_config = ""
  new_config += description if description

  new_config + "#{key}: #{value}"
end

#get_plugin_configs(default_config) ⇒ String

Get plugin configs

Parameters:

  • default_config (String)

    Existing configuration

Returns:

  • (String)

    default_config with plugin configurations added



246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/searchlink/config.rb', line 246

def get_plugin_configs(default_config)
  SL::Searches.plugins[:search].each_value do |plugin|
    next unless plugin.key?(:config) && !plugin[:config].nil? && !plugin[:config].empty?

    plugin[:config].each do |cfg|
      new_config = get_plugin_config(cfg)

      default_config += new_config
    end
  end
  default_config
end

#help_cliObject



101
102
103
# File 'lib/searchlink/help.rb', line 101

def help_cli
  $stdout.puts help_text
end

#help_cssObject



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

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

#help_dialogObject



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

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



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

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



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
59
60
# File 'lib/searchlink/help.rb', line 28

def help_js
  "    function filterTable() {\n      let input, filter, table, tr, i, txtValue;\n      input = document.getElementById(\"filter\");\n      filter = input.value.toUpperCase();\n      table = document.getElementById(\"searches\");\n      table2 = document.getElementById(\"custom\");\n\n      tr = table.getElementsByTagName(\"tr\");\n\n      for (i = 0; i < tr.length; i++) {\n          txtValue = tr[i].textContent || tr[i].innerText;\n          if (txtValue.toUpperCase().indexOf(filter) > -1) {\n            tr[i].style.display = \"\";\n          } else {\n            tr[i].style.display = \"none\";\n          }\n      }\n\n      tr = table2.getElementsByTagName(\"tr\");\n\n      for (i = 0; i < tr.length; i++) {\n          txtValue = tr[i].textContent || tr[i].innerText;\n          if (txtValue.toUpperCase().indexOf(filter) > -1) {\n            tr[i].style.display = \"\";\n          } else {\n            tr[i].style.display = \"none\";\n          }\n      }\n    }\n  EOJS\nend\n"

#help_textObject



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

def help_text
  text = "    -- [Available searches] -------------------\n    \#{SL::Searches.available_searches}\n  EOHELP\n\n  if SL.config[\"custom_site_searches\"]\n    text += \"\\n-- [Custom Searches] ----------------------\\n\"\n    SL.config[\"custom_site_searches\"].sort_by { |l, _s| l }.each { |label, site| text += \"!\#{label}\#{label.spacer} \#{site}\\n\" }\n  end\n  text\nend\n"

#parse(input) ⇒ Object

Parse the input string and perform searches



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
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
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
# File 'lib/searchlink/parse.rb', line 13

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 = []
  SL.shortener = :none

  # 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 SL.config["complete_bare"]
    rx = %r{(?ix-m)(?<!\(|:\s|<)(?:
            (?:https?://)(?:[\da-z.-]+)\.(?:[a-z.]{2,6})
            (?:[/\w\d.\-()_/+=?&%]*?(?=[\s\n]|$))
          )}
    input.gsub!(rx) do
      url_match = Regexp.last_match
      url_match.pre_match =~ /!\S+ +$/ ? url_match[0] : "[%](#{url_match[0]})"
    end
  end

  if input =~ /\[\n(.*?\n)+\]\((.*?)?\)/
    input.gsub!(/\[\n(((\s*(?:[-+*]|\d+\.)?\s+)*(!\S+ +)?(.*?))\n)+\]\((!\S+.*?)?\)/) do
      m = Regexp.last_match
      lines = m[0].split(/\n/)
      lines = lines[1..-2]
      lines.map do |l|
        el_rx = /(\s*(?:[-+*]|\d+\.)?\s+)?(!\S+ )?(\w.*?)$/
        if l =~ el_rx
          els = l.match(el_rx)
          search = if els[2]
              els[2].strip
            else
              m[6] || "!g"
            end
          "#{els[1]}[#{els[3].strip}](#{search})"
        else
          l
        end
      end.join("\n")
    end
  end

  # Handle links in the form of [text](url) or [text](url "title")
  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 =~ /^\s*[~`]{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 || ""
        query, link_info = link_info.extract_query({})

        if @link_text.strip == "" && link_info =~ /".*?"/
          link_info.gsub!(/"(.*?)"/) do
            m = Regexp.last_match
            @link_text = m[1] if @link_text == ""
            m[0]
          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

        if @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).extract_shortener
          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
          create_footnote(m)
          # Handle [](URL) and [%](URL), filling in title
        elsif (@link_text == "" || @link_text == "%") && SL::URL.url?(link_info)
          add_title(link_info)
        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 = if m[1].nil?
                SL::GoogleSearch.api_key? ? "gg" : "g"
              else
                m[1]
              end

            search_type.extract_shortener!

            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
            if search_terms.strip =~ /^\+[^+]/
              search_terms = "#{@link_text} #{search_terms.strip.sub(/^\+\s*/, "")}"
            end

            # 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 = search_terms.sub(/(!!)?\^?(!!)?$/, "")

            if search_type =~ /^(\S+\.)+\S+$/
              search_type = "g"
              search_terms = "site:#{m[1]} #{search_terms}"
            end
          elsif link_info =~ /^!/
            search_word = link_info.match(/^!(\S+)/)
            st = search_word[1].extract_shortener
            if search_word && SL::Searches.valid_search?(st)
              search_type = st unless search_word.nil?
              search_terms = @link_text
            elsif search_word && st =~ /^(\S+\.)+\S+$/
              search_type = SL::GoogleSearch.api_key? ? "gg" : "g"
              search_terms = "site:#{search_word[1]} #{@link_text}"
            else
              SL.add_error("Invalid search#{SL::Searches.did_you_mean(st)}", match)
              search_type = false
              search_terms = false
            end
          elsif @link_text && !@link_text.empty? && (!link_info || link_info.empty?)
            search_type = SL::GoogleSearch.api_key? ? "gg" : "g"
            search_terms = @link_text
          elsif link_info && !link_info.empty?
            search_type = SL::GoogleSearch.api_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?
            search_type, search_terms = custom_search(search_type, search_terms)
          end

          SL.add_query(query) if query

          if (search_type && search_terms) || @url
            # warn "Searching #{search_type} for #{search_terms}"

            @search_count += 1
            @url, title, @link_text = do_search(search_type, search_terms, @link_text, @search_count)

            if (@link_text == "" || @link_text == "%") && @url
              if title
                @link_text = title
              else
                add_title(@url)
              end
            end

            if @url
              res = confirmed?(@url)
              if !res
                return match
              else
                @url = res if res.is_a?(String) && SL::URL.url?(res)
              end

              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 # Assume single line input
    link_only = false
    SL.clipboard = false

    res = parse_arguments(input.strip!).strip
    input = res.nil? ? input.strip : res
    query, input = input.extract_query({})

    # 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).extract_shortener
      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
          type, terms = custom_search(type, terms)
        end

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

        SL.add_query(query) if query
        @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)
      type ||= if Regexp.last_match(2) =~ /[a-z0-9_]@[a-z0-9_.]+/i
          "m"
        else
          "t"
        end
      @link_text = input.sub(/^[tfilm]/, "")
      SL.add_query(query) if query
      @url, title = SL::SocialSearch.social_handle(type, @link_text)
      @link_text = title
    else
      SL.add_query(query) if query
      @link_text ||= input
      @url, title, @link_text = SL.ddg(input, @link_text)
    end

    if @url
      res = confirmed?(@url)
      if res
        if res.is_a?(String) && SL::URL.url?(res)
          @url = res
          title = SL::URL.title(@url) unless title == ""
        end

        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("Canceled", "User canceled result #{@url}")
        SL.add_output SL.originput.chomp
        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

#restore_prev_configObject

Reset configuration



296
297
298
299
300
301
302
# File 'lib/searchlink/config.rb', line 296

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

#write_new_plugin_configObject

Add new keys to config if don’t exist



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/searchlink/config.rb', line 221

def write_new_plugin_config
  default_config = IO.read(config_file)
  new_config = ""
  SL::Searches.plugins[:search].each_value do |plugin|
    next unless plugin.key?(:config) && !plugin[:config].nil? && !plugin[:config].empty?

    plugin[:config].each do |cfg|
      next if default_config =~ /^(# *)?#{cfg[:key]}:/

      new_config += get_plugin_config(cfg)
    end
  end

  return if new_config.empty?

  File.open(config_file, "w") { |f| f.puts default_config + new_config }
end