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,166 @@
1
+ #import
2
+ require_relative 'helpers/chromium'
3
+
4
+ #import
5
+ require_relative 'helpers/firefox'
6
+
7
+ #import
8
+ require_relative 'helpers/safari'
9
+
10
+ module SL
11
+ # Browser history/bookmark search
12
+ class HistorySearch
13
+ class << self
14
+ def settings
15
+ {
16
+ trigger: 'h(([scfabe])([hb])?)*',
17
+ searches: [
18
+ ['h', 'Browser History/Bookmark Search'],
19
+ ['hsh', 'Safari History Search'],
20
+ ['hsb', 'Safari Bookmark Search'],
21
+ ['hshb', nil],
22
+ ['hsbh', nil],
23
+ ['hch', 'Chrome History Search'],
24
+ ['hcb', 'Chrome Bookmark Search'],
25
+ ['hchb', nil],
26
+ ['hcbh', nil],
27
+ ['hfh', 'Firefox History Search'],
28
+ ['hfb', 'Firefox Bookmark Search'],
29
+ ['hfhb', nil],
30
+ ['hfbh', nil],
31
+ ['hah', 'Arc History Search'],
32
+ ['hab', 'Arc Bookmark Search'],
33
+ ['hahb', nil],
34
+ ['habh', nil],
35
+ ['hbh', 'Brave History Search'],
36
+ ['hbb', 'Brave Bookmark Search'],
37
+ ['hbhb', nil],
38
+ ['hbbh', nil],
39
+ ['heh', 'Edge History Search'],
40
+ ['heb', 'Edge Bookmark Search'],
41
+ ['hehb', nil],
42
+ ['hebh', nil]
43
+ ]
44
+ }
45
+ end
46
+
47
+ def search(search_type, search_terms, link_text)
48
+ str = search_type.match(/^h(([scfabe])([hb])?)*$/)[1]
49
+
50
+ types = []
51
+ while str && str.length.positive?
52
+ if str =~ /^s([hb]*)/
53
+ t = Regexp.last_match(1)
54
+ if t.length > 1 || t.empty?
55
+ types.push('safari_history')
56
+ types.push('safari_bookmarks')
57
+ elsif t == 'h'
58
+ types.push('safari_history')
59
+ elsif t == 'b'
60
+ types.push('safari_bookmarks')
61
+ end
62
+ str.sub!(/^s([hb]*)/, '')
63
+ end
64
+
65
+ if str =~ /^c([hb]*)/
66
+ t = Regexp.last_match(1)
67
+ if t.length > 1 || t.empty?
68
+ types.push('chrome_bookmarks')
69
+ types.push('chrome_history')
70
+ elsif t == 'h'
71
+ types.push('chrome_history')
72
+ elsif t == 'b'
73
+ types.push('chrome_bookmarks')
74
+ end
75
+ str.sub!(/^c([hb]*)/, '')
76
+ end
77
+
78
+ if str =~ /^f([hb]*)$/
79
+ t = Regexp.last_match(1)
80
+ if t.length > 1 || t.empty?
81
+ types.push('firefox_bookmarks')
82
+ types.push('firefox_history')
83
+ elsif t == 'h'
84
+ types.push('firefox_history')
85
+ elsif t == 'b'
86
+ types.push('firefox_bookmarks')
87
+ end
88
+ str.sub!(/^f([hb]*)/, '')
89
+ end
90
+
91
+ if str =~ /^e([hb]*)$/
92
+ t = Regexp.last_match(1)
93
+ if t.length > 1 || t.empty?
94
+ types.push('edge_bookmarks')
95
+ types.push('edge_history')
96
+ elsif t == 'h'
97
+ types.push('edge_history')
98
+ elsif t == 'b'
99
+ types.push('edge_bookmarks')
100
+ end
101
+ str.sub!(/^e([hb]*)/, '')
102
+ end
103
+
104
+ if str =~ /^b([hb]*)$/
105
+ t = Regexp.last_match(1)
106
+ if t.length > 1 || t.empty?
107
+ types.push('brave_bookmarks')
108
+ types.push('brave_history')
109
+ elsif t == 'h'
110
+ types.push('brave_history')
111
+ elsif t == 'b'
112
+ types.push('brave_bookmarks')
113
+ end
114
+ str.sub!(/^b([hb]*)/, '')
115
+ end
116
+
117
+ next unless str =~ /^a([hb]*)$/
118
+
119
+ t = Regexp.last_match(1)
120
+ if t.length > 1 || t.empty?
121
+ types.push('arc_bookmarks')
122
+ types.push('arc_history')
123
+ elsif t == 'h'
124
+ types.push('arc_history')
125
+ elsif t == 'b'
126
+ types.push('arc_bookmarks')
127
+ end
128
+ str.sub!(/^a([hb]*)/, '')
129
+ end
130
+
131
+ url, title = search_history(search_terms, types)
132
+ link_text = title if link_text == '' || link_text == search_terms
133
+ [url, title, link_text]
134
+ end
135
+
136
+ def search_history(term, types = [])
137
+ if types.empty?
138
+ return false unless SL.config['history_types']
139
+
140
+ types = SL.config['history_types']
141
+ end
142
+
143
+ results = []
144
+
145
+ if !types.empty?
146
+ types.each do |type|
147
+ url, title, date = send("search_#{type}", term)
148
+
149
+ results << { 'url' => url, 'title' => title, 'date' => date } if url
150
+ end
151
+
152
+ if results.empty?
153
+ false
154
+ else
155
+ out = results.sort_by! { |r| r['date'] }.last
156
+ [out['url'], out['title']]
157
+ end
158
+ else
159
+ false
160
+ end
161
+ end
162
+ end
163
+
164
+ SL::Searches.register 'history', :search, self
165
+ end
166
+ end
@@ -0,0 +1,77 @@
1
+ module SL
2
+ #
3
+ # Hookmark String helpers
4
+ #
5
+ class ::String
6
+ def split_hook
7
+ elements = split(/\|\|/)
8
+ {
9
+ name: elements[0].nil_if_missing,
10
+ url: elements[1].nil_if_missing,
11
+ path: elements[2].nil_if_missing
12
+ }
13
+ end
14
+
15
+ def split_hooks
16
+ split(/\^\^/).map(&:split_hook)
17
+ end
18
+ end
19
+
20
+ ##
21
+ ## Hookmark Search
22
+ ##
23
+ class HookSearch
24
+ class << self
25
+ def settings
26
+ {
27
+ trigger: 'hook',
28
+ searches: [
29
+ ['hook', 'Hookmark Bookmark Search']
30
+ ]
31
+ }
32
+ end
33
+
34
+ # Main search method
35
+ def search(_, search_terms, link_text)
36
+ url, title = search_hook(search_terms)
37
+ [url, title, link_text]
38
+ end
39
+
40
+ ##
41
+ ## Run the AppleScript Hookmark query
42
+ ##
43
+ ## @param query [String] The query
44
+ ##
45
+ def run_query(query)
46
+ `osascript <<'APPLESCRIPT'
47
+ tell application "Hook"
48
+ set _marks to every bookmark whose #{query}
49
+ set _out to {}
50
+ repeat with _hook in _marks
51
+ set _out to _out & (name of _hook & "||" & address of _hook & "||" & path of _hook)
52
+ end repeat
53
+ set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "^^"}
54
+ set _output to _out as string
55
+ set AppleScript's text item delimiters to astid
56
+ return _output
57
+ end tell
58
+ APPLESCRIPT`.strip.split_hooks
59
+ end
60
+
61
+ # Search bookmark paths and addresses. Return array of bookmark hashes.
62
+ def search_hook(search)
63
+ types = %w[name path address]
64
+ query = search.strip.split(' ').map { |s| types.map { |t| %(#{t} contains "#{s}") }.join(' or ') }
65
+ query = query.map { |q| "(#{q})" }.join(' and ')
66
+ path_matches = run_query(query)
67
+
68
+ top_match = path_matches.uniq.first
69
+ return false unless top_match
70
+
71
+ [top_match[:url], top_match[:name]]
72
+ end
73
+ end
74
+
75
+ SL::Searches.register 'hook', :search, self
76
+ end
77
+ end
@@ -0,0 +1,97 @@
1
+ # title: iTunes Search
2
+ # description: Search iTunes
3
+ module SL
4
+ class ITunesSearch
5
+ class << self
6
+ def settings
7
+ {
8
+ trigger: '(i(pod|art|alb|song|tud?)|masd?)',
9
+ searches: [
10
+ ['ipod', 'iTunes podcast'],
11
+ ['iart', 'iTunes artist'],
12
+ ['ialb', 'iTunes album'],
13
+ ['isong', 'iTunes song'],
14
+ ['itu', 'iOS App Store Search'],
15
+ ['itud', 'iOS App Store Developer Link'],
16
+ ['mas', 'Mac App Store Search'],
17
+ ['masd', 'Mac App Store Developer Link']
18
+ ]
19
+ }
20
+ end
21
+
22
+ def search(search_type, search_terms, link_text)
23
+ case search_type
24
+ when /^ialb$/ # iTunes Album Search
25
+ url, title = search_itunes('album', search_terms, false)
26
+ when /^iart$/ # iTunes Artist Search
27
+ url, title = search_itunes('musicArtist', search_terms, false)
28
+ when /^imov?$/ # iTunes movie search
29
+ dev = false
30
+ url, title = search_itunes('movie', search_terms, dev, SL.config['itunes_affiliate'])
31
+ when /^ipod$/
32
+ url, title = search_itunes('podcast', search_terms, false)
33
+ when /^isong$/ # iTunes Song Search
34
+ url, title = search_itunes('song', search_terms, false)
35
+ when /^itud?$/ # iTunes app search
36
+ dev = search_type =~ /d$/
37
+ url, title = search_itunes('iPadSoftware', search_terms, dev, SL.config['itunes_affiliate'])
38
+ when /^masd?$/ # Mac App Store search (mas = itunes link, masd = developer link)
39
+ dev = search_type =~ /d$/
40
+ url, title = search_itunes('macSoftware', search_terms, dev, SL.config['itunes_affiliate'])
41
+ end
42
+
43
+ [url, title, link_text]
44
+ end
45
+
46
+ def search_itunes(entity, terms, dev, aff = nil)
47
+ aff ||= SL.config['itunes_affiliate']
48
+
49
+ url = "http://itunes.apple.com/search?term=#{terms.url_encode}&country=#{SL.config['country_code']}&entity=#{entity}&limit=1"
50
+
51
+ begin
52
+ page = Curl::Json.new(url, compressed: true)
53
+ json = page.json
54
+ rescue StandardError => e
55
+ SL.add_error('Invalid response', "Search for #{terms}: (#{e})")
56
+ return false
57
+ end
58
+ return false unless json
59
+
60
+ return false unless json['resultCount']&.positive?
61
+
62
+ result = json['results'][0]
63
+ case entity
64
+ when /movie/
65
+ # dev parameter probably not necessary in this case
66
+ output_url = result['trackViewUrl']
67
+ output_title = result['trackName']
68
+ when /(mac|iPad)Software/
69
+ output_url = dev && result['sellerUrl'] ? result['sellerUrl'] : result['trackViewUrl']
70
+ output_title = result['trackName']
71
+ when /(musicArtist|song|album)/
72
+ case result['wrapperType']
73
+ when 'track'
74
+ output_url = result['trackViewUrl']
75
+ output_title = "#{result['trackName']} by #{result['artistName']}"
76
+ when 'collection'
77
+ output_url = result['collectionViewUrl']
78
+ output_title = "#{result['collectionName']} by #{result['artistName']}"
79
+ when 'artist'
80
+ output_url = result['artistLinkUrl']
81
+ output_title = result['artistName']
82
+ end
83
+ when /podcast/
84
+ output_url = result['collectionViewUrl']
85
+ output_title = result['collectionName']
86
+ end
87
+ return false unless output_url && output_title
88
+
89
+ return [output_url, output_title] if dev
90
+
91
+ [output_url + aff, output_title]
92
+ end
93
+ end
94
+
95
+ SL::Searches.register 'itunes', :search, self
96
+ end
97
+ end
@@ -0,0 +1,41 @@
1
+ module SL
2
+ class LastFMSearch
3
+ class << self
4
+ def settings
5
+ {
6
+ trigger: 'l(art|song)',
7
+ searches: [
8
+ ['lart', 'Last.fm Artist Search'],
9
+ ['lsong', 'Last.fm Song Search']
10
+ ]
11
+ }
12
+ end
13
+
14
+ def search(search_type, search_terms, link_text)
15
+ type = search_type =~ /art$/ ? 'artist' : 'track'
16
+
17
+ url = "http://ws.audioscrobbler.com/2.0/?method=#{type}.search&#{type}=#{search_terms.url_encode}&api_key=2f3407ec29601f97ca8a18ff580477de&format=json"
18
+ json = Curl::Json.new(url).json
19
+ return false unless json['results']
20
+
21
+ begin
22
+ case type
23
+ when 'track'
24
+ result = json['results']['trackmatches']['track'][0]
25
+ url = result['url']
26
+ title = "#{result['name']} by #{result['artist']}"
27
+ when 'artist'
28
+ result = json['results']['artistmatches']['artist'][0]
29
+ url = result['url']
30
+ title = result['name']
31
+ end
32
+ [url, title, link_text]
33
+ rescue StandardError
34
+ false
35
+ end
36
+ end
37
+ end
38
+
39
+ SL::Searches.register 'lastfm', :search, self
40
+ end
41
+ end
@@ -0,0 +1,91 @@
1
+ # Always start with module SL
2
+ module SL
3
+ # Give it a unique class name
4
+ class LyricsSearch
5
+ class << self
6
+ # Settings block is required with `trigger` and `searches`
7
+ def settings
8
+ {
9
+ # `trigger` is A regular expression that will trigger this plugin
10
+ # when used with a bang. The one below will trigger on !lyrics or
11
+ # !lyricse.
12
+ trigger: 'lyrics?e?',
13
+ # Every search that the plugin should execute should be individually
14
+ # listed and described in the searches array. This is used for
15
+ # completion and help generation. Do not include the bang (!) in the
16
+ # search keyword.
17
+ searches: [
18
+ ['lyric', 'Song Lyrics Search'],
19
+ ['lyrice', 'Song Lyrics Embed']
20
+ ]
21
+ }
22
+ end
23
+
24
+ # Every plugin must contain a #search method that takes 3 arguments:
25
+ #
26
+ # - `search_type` will contain the !search trigger that was used (minus the !)
27
+ # - `search_terms` will include everything that came after the !search
28
+ # - `link_text` will contain the text that will be used for the linked
29
+ # text portion of the link. This can usually remain untouched but must
30
+ # be passed back at the end of the function.
31
+ def search(search_type, search_terms, link_text)
32
+ # You can branch to multiple searches by testing the search_type
33
+ case search_type
34
+ when /e$/
35
+ url, title = SL.ddg("site:genius.com #{search_terms}", link_text)
36
+ if url
37
+ title = get_lyrics(url)
38
+ # To return an embed, set url (first parameter in the return
39
+ # array) to 'embed', and put the embed contents in the second
40
+ # parameter.
41
+ title ? ['embed', title, link_text] : false
42
+ else
43
+ # Use `SL#add_error(title, text)` to add errors to the HTML
44
+ # report. The report will only be shown if errors have been added.
45
+ SL.add_error('No lyrics found', "Song lyrics for #{search_terms} not found")
46
+ false
47
+ end
48
+ else
49
+ # You can perform a DuckDuckGo search using SL#ddg, passing the
50
+ # search terms and link_text. It will return url, title, and
51
+ # link_text. SL#ddg will add its own errors, and if it returns false
52
+ # that will automatically be tested for, no additional error
53
+ # handling is required.
54
+ url, title, link_text = SL.ddg("site:genius.com #{search_terms}", link_text)
55
+ # Always return an array containing the resulting URL, the title,
56
+ # and the link_text variable that was passed in, even if it's
57
+ # unmodified.
58
+ [url, title, link_text]
59
+ end
60
+ end
61
+
62
+ # Any additional helper methods can be defined after #search
63
+ def get_lyrics(url)
64
+ if SL::URL.valid_link?(url)
65
+ # You can use Ruby's net/http methods for retrieving pages, but
66
+ # `curl -SsL` is faster and easier. Curl::Html.new(url) returns a
67
+ # new object containing :body
68
+ body = Curl::Html.new(url).body
69
+
70
+ matches = body.scan(%r{class="Lyrics__Container-.*?>(.*?)</div><div class="LyricsFooter})
71
+
72
+ lyrics = matches.join("\n")
73
+
74
+ if lyrics
75
+ "```\n#{CGI.unescape(lyrics).gsub(%r{<br/?>}, " \n").gsub(%r{</?.*?>}, '').gsub(/&#x27;/, "'")}\n```"
76
+ else
77
+ false
78
+ end
79
+ else
80
+ false
81
+ end
82
+ end
83
+ end
84
+
85
+ # At the end of the search class, you must register it as a plugin. This
86
+ # method takes a title, a type (:search for a search plugin), and the
87
+ # unique class. When running #register within the search class itself,
88
+ # you can just use `self`.
89
+ SL::Searches.register 'lyrics', :search, self
90
+ end
91
+ end
@@ -0,0 +1,183 @@
1
+ module SL
2
+ class PinboardSearch
3
+ PINBOARD_CACHE = SL::Util.cache_file_for('pinboard')
4
+
5
+ class << self
6
+ def settings
7
+ {
8
+ trigger: 'pb',
9
+ searches: [
10
+ ['pb', 'Pinboard Bookmark Search']
11
+ ]
12
+ }
13
+ end
14
+
15
+ def pinboard_bookmarks
16
+ curl = TTY::Which.which('curl')
17
+ bookmarks = `#{curl} -sSL "https://api.pinboard.in/v1/posts/all?auth_token=#{SL.config['pinboard_api_key']}&format=json"`
18
+ bookmarks = bookmarks.force_encoding('utf-8')
19
+ bookmarks.gsub!(/[^[:ascii:]]/) do |non_ascii|
20
+ non_ascii.force_encoding('utf-8')
21
+ .encode('utf-16be')
22
+ .unpack('H*')
23
+ .gsub(/(....)/, '\u\1')
24
+ end
25
+
26
+ bookmarks.gsub!(/[\u{1F600}-\u{1F6FF}]/, '')
27
+
28
+ bookmarks = JSON.parse(bookmarks)
29
+ updated = Time.now
30
+ { 'update_time' => updated, 'bookmarks' => bookmarks }
31
+ end
32
+
33
+ def save_pinboard_cache(cache)
34
+ cachefile = PINBOARD_CACHE
35
+
36
+ # file = File.new(cachefile,'w')
37
+ # file = Zlib::GzipWriter.new(File.new(cachefile,'w'))
38
+ begin
39
+ File.open(cachefile, 'wb') { |f| f.write(Marshal.dump(cache)) }
40
+ rescue IOError
41
+ SL.add_error('Pinboard cache error', 'Failed to write stash to disk')
42
+ return false
43
+ end
44
+ true
45
+ end
46
+
47
+ def load_pinboard_cache
48
+ refresh_cache = false
49
+ cachefile = PINBOARD_CACHE
50
+
51
+ if File.exist?(cachefile)
52
+ begin
53
+ # file = IO.read(cachefile) # Zlib::GzipReader.open(cachefile)
54
+ # cache = Marshal.load file
55
+ cache = Marshal.load(File.binread(cachefile))
56
+ # file.close
57
+ rescue IOError # Zlib::GzipFile::Error
58
+ SL.add_error('Error loading pinboard cache', "IOError reading #{cachefile}")
59
+ cache = pinboard_bookmarks
60
+ save_pinboard_cache(cache)
61
+ rescue StandardError
62
+ SL.add_error('Error loading pinboard cache', "StandardError reading #{cachefile}")
63
+ cache = pinboard_bookmarks
64
+ save_pinboard_cache(cache)
65
+ end
66
+ curl = TTY::Which.which('curl')
67
+ updated = JSON.parse(`#{curl} -SsL 'https://api.pinboard.in/v1/posts/update?auth_token=#{SL.config['pinboard_api_key']}&format=json'`)
68
+ last_bookmark = Time.parse(updated['update_time'])
69
+ if cache&.key?('update_time')
70
+ last_update = cache['update_time']
71
+ refresh_cache = true if last_update < last_bookmark
72
+ else
73
+ refresh_cache = true
74
+ end
75
+ else
76
+ refresh_cache = true
77
+ end
78
+
79
+ if refresh_cache
80
+ cache = pinboard_bookmarks
81
+ save_pinboard_cache(cache)
82
+ end
83
+
84
+ cache
85
+ end
86
+
87
+ # Search pinboard bookmarks
88
+ # Begin query with '' to force exact matching (including description text)
89
+ # Regular matching searches for each word of query and scores the bookmarks
90
+ # exact matches in title get highest score
91
+ # exact matches in description get second highest score
92
+ # other bookmarks are scored based on the number of words that match
93
+ #
94
+ # After sorting by score, bookmarks will be sorted by date and the most recent
95
+ # will be returned
96
+ #
97
+ # Exact matching is case and punctuation insensitive
98
+ def search(_, search_terms, link_text)
99
+ unless SL.config['pinboard_api_key']
100
+ SL.add_error('Missing Pinboard API token',
101
+ 'Find your api key at https://pinboard.in/settings/password and add it
102
+ to your configuration (pinboard_api_key: YOURKEY)')
103
+ return false
104
+ end
105
+
106
+ exact_match = false
107
+ match_phrases = []
108
+
109
+ # If search terms start with ''term, only search for exact string matches
110
+ case search_terms
111
+ when /^ *'/
112
+ exact_match = true
113
+ search_terms.gsub!(/(^ *'+|'+ *$)/, '')
114
+ when /%22(.*?)%22/
115
+ match_phrases = search_terms.scan(/%22(\S.*?\S)%22/)
116
+ search_terms.gsub!(/%22(\S.*?\S)%22/, '')
117
+ end
118
+
119
+ cache = load_pinboard_cache
120
+ # cache = pinboard_bookmarks
121
+ bookmarks = cache['bookmarks']
122
+
123
+ if exact_match
124
+ bookmarks.each do |bm|
125
+ text = [bm['description'], bm['extended'], bm['tags']].join(' ')
126
+
127
+ return [bm['href'], bm['description']] if text.matches_exact(search_terms)
128
+ end
129
+
130
+ return false
131
+ end
132
+
133
+ unless match_phrases.empty?
134
+ bookmarks.delete_if do |bm|
135
+ matched = tru
136
+ full_text = [bm['description'], bm['extended'], bm['tags']].join(' ')
137
+ match_phrases.each do |phrase|
138
+ matched = false unless full_text.matches_exact(phrase)
139
+ end
140
+ !matched
141
+ end
142
+ end
143
+
144
+ matches = []
145
+ bookmarks.each do |bm|
146
+ title_tags = [bm['description'], bm['tags']].join(' ')
147
+ full_text = [bm['description'], bm['extended'], bm['tags']].join(' ')
148
+
149
+ score = if title_tags.matches_exact(search_terms)
150
+ 14.0
151
+ elsif full_text.matches_exact(search_terms)
152
+ 13.0
153
+ elsif full_text.matches_any(search_terms)
154
+ full_text.matches_score(search_terms)
155
+ else
156
+ 0
157
+ end
158
+
159
+ return [bm['href'], bm['description']] if score == 14
160
+
161
+ next unless score.positive?
162
+
163
+ matches.push({
164
+ score: score,
165
+ href: bm['href'],
166
+ title: bm['description'],
167
+ date: bm['time']
168
+ })
169
+ end
170
+
171
+ return false if matches.empty?
172
+
173
+ top = matches.max_by { |bm| [bm[:score], bm[:date]] }
174
+
175
+ return false unless top
176
+
177
+ [top[:href], top[:title], link_text]
178
+ end
179
+ end
180
+
181
+ SL::Searches.register 'pinboard', :search, self
182
+ end
183
+ end