searchlink 2.3.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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