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,318 @@
1
+ ##
2
+ ## Chromium (Chrome, Arc, Brave, Edge) search methods
3
+ ##
4
+ module SL
5
+ # Chromium history search
6
+ class HistorySearch
7
+ class << self
8
+ ## Search Arc history
9
+ ##
10
+ ## @param term The search term
11
+ ##
12
+ ## @return [Array] Single bookmark, [url, title, date]
13
+ ##
14
+ def search_arc_history(term)
15
+ # Google history
16
+ history_file = File.expand_path('~/Library/Application Support/Arc/User Data/Default/History')
17
+ if File.exist?(history_file)
18
+ SL.notify('Searching Arc History', term)
19
+ search_chromium_history(history_file, term)
20
+ else
21
+ false
22
+ end
23
+ end
24
+
25
+ ## Search Brave history
26
+ ##
27
+ ## @param term The search term
28
+ ##
29
+ ## @return [Array] Single bookmark, [url, title, date]
30
+ ##
31
+ def search_brave_history(term)
32
+ # Google history
33
+ history_file = File.expand_path('~/Library/Application Support/BraveSoftware/Brave-Browser/Default/History')
34
+ if File.exist?(history_file)
35
+ SL.notify('Searching Brave History', term)
36
+ search_chromium_history(history_file, term)
37
+ else
38
+ false
39
+ end
40
+ end
41
+
42
+ ## Search Edge history
43
+ ##
44
+ ## @param term The search term
45
+ ##
46
+ ## @return [Array] Single bookmark, [url, title, date]
47
+ ##
48
+ def search_edge_history(term)
49
+ # Google history
50
+ history_file = File.expand_path('~/Library/Application Support/Microsoft Edge/Default/History')
51
+ if File.exist?(history_file)
52
+ SL.notify('Searching Edge History', term)
53
+ search_chromium_history(history_file, term)
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ ## Search Chrome history
60
+ ##
61
+ ## @param term The search term
62
+ ##
63
+ ## @return [Array] Single bookmark, [url, title, date]
64
+ ##
65
+ def search_chrome_history(term)
66
+ # Google history
67
+ history_file = File.expand_path('~/Library/Application Support/Google/Chrome/Default/History')
68
+ if File.exist?(history_file)
69
+ SL.notify('Searching Chrome History', term)
70
+ search_chromium_history(history_file, term)
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ ##
77
+ ## Generic chromium history search
78
+ ##
79
+ ## @param history_file [String] The history file
80
+ ## path for the selected
81
+ ## browser
82
+ ## @param term [String] The search term
83
+ ##
84
+ ## @return [Array] Single bookmark, [url, title, date]
85
+ ##
86
+ def search_chromium_history(history_file, term)
87
+ tmpfile = "#{history_file}.tmp"
88
+ FileUtils.cp(history_file, tmpfile)
89
+
90
+ exact_match = false
91
+ match_phrases = []
92
+
93
+ # If search terms start with ''term, only search for exact string matches
94
+ if term =~ /^ *'/
95
+ exact_match = true
96
+ term.gsub!(/(^ *'+|'+ *$)/, '')
97
+ elsif term =~ /%22(.*?)%22/
98
+ match_phrases = term.scan(/%22(\S.*?\S)%22/)
99
+ term.gsub!(/%22(\S.*?\S)%22/, '')
100
+ end
101
+
102
+ terms = []
103
+ terms.push("(url NOT LIKE '%search/?%'
104
+ AND url NOT LIKE '%?q=%'
105
+ AND url NOT LIKE '%?s=%'
106
+ AND url NOT LIKE '%duckduckgo.com/?t%')")
107
+ if exact_match
108
+ terms.push("(url LIKE '%#{term.strip.downcase}%' OR title LIKE '%#{term.strip.downcase}%')")
109
+ else
110
+ terms.concat(term.split(/\s+/).map do |t|
111
+ "(url LIKE '%#{t.strip.downcase}%' OR title LIKE '%#{t.strip.downcase}%')"
112
+ end)
113
+ terms.concat(match_phrases.map do |t|
114
+ "(url LIKE '%#{t[0].strip.downcase}%' OR title LIKE '%#{t[0].strip.downcase}%')"
115
+ end)
116
+ end
117
+
118
+ query = terms.join(' AND ')
119
+ most_recent = `sqlite3 -json '#{tmpfile}' "select title, url,
120
+ datetime(last_visit_time / 1000000 + (strftime('%s', '1601-01-01')), 'unixepoch') as datum
121
+ from urls where #{query} order by datum desc limit 1 COLLATE NOCASE;"`.strip
122
+ FileUtils.rm_f(tmpfile)
123
+ return false if most_recent.strip.empty?
124
+
125
+ bm = JSON.parse(most_recent)[0]
126
+
127
+ date = Time.parse(bm['datum'])
128
+ [bm['url'], bm['title'], date]
129
+ end
130
+
131
+ ##
132
+ ## Search Arc bookmarks
133
+ ##
134
+ ## @param term [String] The search term
135
+ ##
136
+ ## @return [Array] single bookmark [url, title, date]
137
+ ##
138
+ def search_arc_bookmarks(term)
139
+ bookmarks_file = File.expand_path('~/Library/Application Support/Arc/User Data/Default/Bookmarks')
140
+
141
+ if File.exist?(bookmarks_file)
142
+ SL.notify('Searching Brave Bookmarks', term)
143
+ return search_chromium_bookmarks(bookmarks_file, term)
144
+ end
145
+
146
+ false
147
+ end
148
+
149
+ ##
150
+ ## Search Brave bookmarks
151
+ ##
152
+ ## @param term [String] The search term
153
+ ##
154
+ ## @return [Array] single bookmark [url, title, date]
155
+ ##
156
+ def search_brave_bookmarks(term)
157
+ bookmarks_file = File.expand_path('~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Bookmarks')
158
+
159
+ if File.exist?(bookmarks_file)
160
+ SL.notify('Searching Brave Bookmarks', term)
161
+ return search_chromium_bookmarks(bookmarks_file, term)
162
+ end
163
+
164
+ false
165
+ end
166
+
167
+ ##
168
+ ## Search Ege bookmarks
169
+ ##
170
+ ## @param term [String] The search term
171
+ ##
172
+ ## @return [Array] single bookmark [url, title, date]
173
+ ##
174
+ def search_edge_bookmarks(term)
175
+ bookmarks_file = File.expand_path('~/Library/Application Support/Microsoft Edge/Default/Bookmarks')
176
+
177
+ if File.exist?(bookmarks_file)
178
+ SL.notify('Searching Edge Bookmarks', term)
179
+ return search_chromium_bookmarks(bookmarks_file, term)
180
+ end
181
+
182
+ false
183
+ end
184
+
185
+ ##
186
+ ## Search Chrome bookmarks
187
+ ##
188
+ ## @param term [String] The search term
189
+ ##
190
+ ## @return [Array] single bookmark [url, title, date]
191
+ ##
192
+ def search_chrome_bookmarks(term)
193
+ bookmarks_file = File.expand_path('~/Library/Application Support/Google/Chrome/Default/Bookmarks')
194
+
195
+ if File.exist?(bookmarks_file)
196
+ SL.notify('Searching Chrome Bookmarks', term)
197
+ return search_chromium_bookmarks(bookmarks_file, term)
198
+ end
199
+
200
+ false
201
+ end
202
+
203
+ ##
204
+ ## Generic chromium bookmark search
205
+ ##
206
+ ## @param bookmarks_file [String] The path to
207
+ ## bookmarks file for
208
+ ## selected browser
209
+ ## @param term [String] The term
210
+ ##
211
+ ## @return [Array] single bookmark [url, title, date]
212
+ ##
213
+ def search_chromium_bookmarks(bookmarks_file, term)
214
+ chrome_bookmarks = JSON.parse(IO.read(bookmarks_file))
215
+
216
+ exact_match = false
217
+ match_phrases = []
218
+
219
+ # If search terms start with ''term, only search for exact string matches
220
+ if term =~ /^ *'/
221
+ exact_match = true
222
+ term.gsub!(/(^ *'+|'+ *$)/, '')
223
+ elsif term =~ /%22(.*?)%22/
224
+ match_phrases = term.scan(/%22(\S.*?\S)%22/)
225
+ term.gsub!(/%22(\S.*?\S)%22/, '')
226
+ end
227
+
228
+ if chrome_bookmarks
229
+ roots = chrome_bookmarks['roots']
230
+
231
+ urls = extract_chrome_bookmarks(roots, [], term)
232
+
233
+ unless urls.empty?
234
+ urls.delete_if { |bm| !(bm[:url].matches_exact(term) || bm[:title].matches_exact(term)) } if exact_match
235
+
236
+ if match_phrases
237
+ match_phrases.map! { |phrase| phrase[0] }
238
+ urls.delete_if do |bm|
239
+ matched = true
240
+ match_phrases.each do |phrase|
241
+ matched = false unless bm[:url].matches_exact(phrase) || bm[:title].matches_exact(phrase)
242
+ end
243
+ !matched
244
+ end
245
+ end
246
+
247
+ return false if urls.empty?
248
+
249
+ lastest_bookmark = urls.max_by { |u| u[:score] }
250
+
251
+ return [lastest_bookmark[:url], lastest_bookmark[:title], lastest_bookmark[:date]]
252
+ end
253
+ end
254
+
255
+ false
256
+ end
257
+
258
+ ##
259
+ ## Extract chromium bookmarks from JSON file
260
+ ##
261
+ ## @param json [String] The json data
262
+ ## @param urls [Array] The gathered urls,
263
+ ## appended to recursively
264
+ ## @param term [String] The search term
265
+ ## (optional)
266
+ ##
267
+ ## @return [Array] array of bookmarks
268
+ ##
269
+ def extract_chrome_bookmarks(json, urls = [], term = '')
270
+ if json.instance_of?(Array)
271
+ json.each { |item| urls = extract_chrome_bookmarks(item, urls, term) }
272
+ elsif json.instance_of?(Hash)
273
+ if json.key? 'children'
274
+ urls = extract_chrome_bookmarks(json['children'], urls, term)
275
+ elsif json['type'] == 'url'
276
+ date = Time.at(json['date_added'].to_i / 1000000 + (Time.new(1601, 01, 01).strftime('%s').to_i))
277
+ url = { url: json['url'], title: json['name'], date: date }
278
+ score = score_mark(url, term)
279
+
280
+ if score > 7
281
+ url[:score] = score
282
+ urls << url
283
+ end
284
+ else
285
+ json.each { |_, v| urls = extract_chrome_bookmarks(v, urls, term) }
286
+ end
287
+ else
288
+ return urls
289
+ end
290
+ urls
291
+ end
292
+
293
+ ##
294
+ ## Score bookmark for search term matches
295
+ ##
296
+ ## @param mark [Hash] The bookmark
297
+ ## @param terms [String] The search terms
298
+ ##
299
+ def score_mark(mark, terms)
300
+ return 0 unless mark[:url]
301
+
302
+ score = if mark[:title] && mark[:title].matches_exact(terms)
303
+ 12 + mark[:url].matches_score(terms, start_word: false)
304
+ elsif mark[:url].matches_exact(terms)
305
+ 11
306
+ elsif mark[:title] && mark[:title].matches_score(terms) > 5
307
+ mark[:title].matches_score(terms)
308
+ elsif mark[:url].matches_score(terms, start_word: false)
309
+ mark[:url].matches_score(terms, start_word: false)
310
+ else
311
+ 0
312
+ end
313
+
314
+ score
315
+ end
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,135 @@
1
+ module SL
2
+ class HistorySearch
3
+ class << self
4
+ def search_firefox_history(term)
5
+ # Firefox history
6
+ base = File.expand_path('~/Library/Application Support/Firefox/Profiles')
7
+ Dir.chdir(base)
8
+ profile = Dir.glob('*default-release')
9
+ return false unless profile
10
+
11
+ src = File.join(base, profile[0], 'places.sqlite')
12
+
13
+ exact_match = false
14
+ match_phrases = []
15
+
16
+ # If search terms start with ''term, only search for exact string matches
17
+ case term
18
+ when /^ *'/
19
+ exact_match = true
20
+ term.gsub!(/(^ *'+|'+ *$)/, '')
21
+ when /%22(.*?)%22/
22
+ match_phrases = term.scan(/%22(\S.*?\S)%22/)
23
+ term.gsub!(/%22(\S.*?\S)%22/, '')
24
+ end
25
+
26
+ if File.exist?(src)
27
+ SL.notify('Searching Firefox History', term)
28
+ tmpfile = "#{src}.tmp"
29
+ FileUtils.cp(src, tmpfile)
30
+
31
+ terms = []
32
+ terms.push("(moz_places.url NOT LIKE '%search/?%'
33
+ AND moz_places.url NOT LIKE '%?q=%'
34
+ AND moz_places.url NOT LIKE '%?s=%'
35
+ AND moz_places.url NOT LIKE '%duckduckgo.com/?t%')")
36
+ if exact_match
37
+ terms.push("(moz_places.url LIKE '%#{term.strip.downcase}%' OR moz_places.title LIKE '%#{term.strip.downcase}%')")
38
+ else
39
+ terms.concat(term.split(/\s+/).map do |t|
40
+ "(moz_places.url LIKE '%#{t.strip.downcase}%' OR moz_places.title LIKE '%#{t.strip.downcase}%')"
41
+ end)
42
+ terms.concat(match_phrases.map do |t|
43
+ "(moz_places.url LIKE '%#{t[0].strip.downcase}%' OR moz_places.title LIKE '%#{t[0].strip.downcase}%')"
44
+ end)
45
+ end
46
+ query = terms.join(' AND ')
47
+ most_recent = `sqlite3 -json '#{tmpfile}' "select moz_places.title, moz_places.url,
48
+ datetime(moz_historyvisits.visit_date/1000000, 'unixepoch', 'localtime') as datum
49
+ from moz_places, moz_historyvisits where moz_places.id = moz_historyvisits.place_id
50
+ and #{query} order by datum desc limit 1 COLLATE NOCASE;"`.strip
51
+ FileUtils.rm_f(tmpfile)
52
+
53
+ return false if most_recent.strip.empty?
54
+
55
+ marks = JSON.parse(most_recent)
56
+
57
+ marks.map! do |bm|
58
+ date = Time.parse(bm['datum'])
59
+ score = score_mark({url: bm['url'], title: bm['title']}, term)
60
+ { url: bm['url'], title: bm['title'], date: date, score: score }
61
+ end
62
+
63
+
64
+ m = marks.sort_by { |m| [m[:url].length * -1, m[:score]] }.last
65
+
66
+ [m[:url], m[:title], m[:date]]
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def search_firefox_bookmarks(term)
73
+ # Firefox history
74
+ base = File.expand_path('~/Library/Application Support/Firefox/Profiles')
75
+ Dir.chdir(base)
76
+ profile = Dir.glob('*default-release')
77
+ return false unless profile
78
+
79
+ src = File.join(base, profile[0], 'places.sqlite')
80
+
81
+ exact_match = false
82
+ match_phrases = []
83
+
84
+ # If search terms start with ''term, only search for exact string matches
85
+ if term =~ /^ *'/
86
+ exact_match = true
87
+ term.gsub!(/(^ *'+|'+ *$)/, '')
88
+ elsif term =~ /%22(.*?)%22/
89
+ match_phrases = term.scan(/%22(\S.*?\S)%22/)
90
+ term.gsub!(/%22(\S.*?\S)%22/, '')
91
+ end
92
+
93
+ if File.exist?(src)
94
+ SL.notify('Searching Firefox Bookmarks', term)
95
+ tmpfile = "#{src}.tmp"
96
+ FileUtils.cp(src, tmpfile)
97
+
98
+ terms = []
99
+ terms.push("(h.url NOT LIKE '%search/?%'
100
+ AND h.url NOT LIKE '%?q=%'
101
+ AND h.url NOT LIKE '%?s=%'
102
+ AND h.url NOT LIKE '%duckduckgo.com/?t%')")
103
+ if exact_match
104
+ terms.push("(h.url LIKE '%#{term.strip.downcase}%' OR h.title LIKE '%#{term.strip.downcase}%')")
105
+ else
106
+ terms.concat(term.split(/\s+/).map do |t|
107
+ "(h.url LIKE '%#{t.strip.downcase}%' OR h.title LIKE '%#{t.strip.downcase}%')"
108
+ end)
109
+ terms.concat(match_phrases.map do |t|
110
+ "(h.url LIKE '%#{t[0].strip.downcase}%' OR h.title LIKE '%#{t[0].strip.downcase}%')"
111
+ end)
112
+ end
113
+
114
+ query = terms.join(' AND ')
115
+
116
+ most_recent = `sqlite3 -json '#{tmpfile}' "select h.url, b.title,
117
+ datetime(b.dateAdded/1000000, 'unixepoch', 'localtime') as datum
118
+ FROM moz_places h JOIN moz_bookmarks b ON h.id = b.fk
119
+ where #{query} order by datum desc limit 1 COLLATE NOCASE;"`.strip
120
+ FileUtils.rm_f(tmpfile)
121
+
122
+ return false if most_recent.strip.empty?
123
+
124
+ bm = JSON.parse(most_recent)[0]
125
+
126
+ date = Time.parse(bm['datum'])
127
+ score = score_mark({url: bm['url'], title: bm['title']}, term)
128
+ [bm['url'], bm['title'], date, score]
129
+ else
130
+ false
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,133 @@
1
+ module SL
2
+ class HistorySearch
3
+ class << self
4
+ # Search Safari history for terms
5
+ #
6
+ # @param term The search term
7
+ #
8
+ def search_safari_history(term)
9
+ # Safari
10
+ src = File.expand_path('~/Library/Safari/History.db')
11
+ if File.exist?(src)
12
+ SL.notify('Searching Safari History', term)
13
+
14
+ exact_match = false
15
+ match_phrases = []
16
+
17
+ # If search terms start with ''term, only search for exact string matches
18
+ if term =~ /^ *'/
19
+ exact_match = true
20
+ term.gsub!(/(^ *'+|'+ *$)/, '')
21
+ elsif term =~ /%22(.*?)%22/
22
+ match_phrases = term.scan(/%22(\S.*?\S)%22/)
23
+ term.gsub!(/%22(\S.*?\S)%22/, '')
24
+ end
25
+
26
+ terms = []
27
+ terms.push("(url NOT LIKE '%search/?%'
28
+ AND url NOT LIKE '%?q=%' AND url NOT LIKE '%?s=%'
29
+ AND url NOT LIKE '%duckduckgo.com/?t%')")
30
+ if exact_match
31
+ terms.push("(url LIKE '%#{term.strip.downcase}%' OR title LIKE '%#{term.strip.downcase}%')")
32
+ else
33
+ terms.concat(term.split(/\s+/).map do |t|
34
+ "(url LIKE '%#{t.strip.downcase}%' OR title LIKE '%#{t.strip.downcase}%')"
35
+ end)
36
+ terms.concat(match_phrases.map do |t|
37
+ "(url LIKE '%#{t[0].strip.downcase}%' OR title LIKE '%#{t[0].strip.downcase}%')"
38
+ end)
39
+ end
40
+
41
+ query = terms.join(' AND ')
42
+
43
+ cmd = %(sqlite3 -json '#{src}' "select title, url,
44
+ datetime(visit_time/1000000, 'unixepoch', 'localtime') as datum
45
+ from history_visits INNER JOIN history_items ON history_items.id = history_visits.history_item
46
+ where #{query} order by datum desc limit 1 COLLATE NOCASE;")
47
+
48
+ most_recent = `#{cmd}`.strip
49
+
50
+ return false if most_recent.strip.empty?
51
+
52
+ bm = JSON.parse(most_recent)[0]
53
+ date = Time.parse(bm['datum'])
54
+ [bm['url'], bm['title'], date]
55
+ else
56
+ false
57
+ end
58
+ end
59
+
60
+ ##
61
+ ## Search Safari bookmarks for relevant search terms
62
+ ##
63
+ ## @param terms [String] The search terms
64
+ ##
65
+ ## @return [Array] [url, title, date]
66
+ ##
67
+ def search_safari_bookmarks(terms)
68
+ data = `plutil -convert xml1 -o - ~/Library/Safari/Bookmarks.plist`.strip
69
+ parent = Plist.parse_xml(data)
70
+ results = get_safari_bookmarks(parent, terms)
71
+ return false if results.empty?
72
+
73
+ result = results.max_by { |res| [res[:score], res[:title].length] }
74
+
75
+ [result[:url], result[:title], Time.now]
76
+ end
77
+
78
+ ##
79
+ ## Score bookmark for search term matches
80
+ ##
81
+ ## @param mark [Hash] The bookmark
82
+ ## @param terms [String] The search terms
83
+ ##
84
+ def score_bookmark(mark, terms)
85
+ score = if mark[:title].matches_exact(terms)
86
+ 12 + mark[:url].matches_score(terms, start_word: false)
87
+ elsif mark[:url].matches_exact(terms)
88
+ 11
89
+ elsif mark[:title].matches_score(terms) > 5
90
+ mark[:title].matches_score(terms)
91
+ elsif mark[:url].matches_score(terms, start_word: false)
92
+ mark[:url].matches_score(terms, start_word: false)
93
+ end
94
+
95
+ { url: mark[:url], title: mark[:title], score: score }
96
+ end
97
+
98
+ ##
99
+ ## Recursively parse bookmarks hash and score
100
+ ## bookmarks
101
+ ##
102
+ ## @param parent [Hash, Array] The parent
103
+ ## bookmark item
104
+ ## @param terms [String] The search terms
105
+ ##
106
+ ## @return [Array] array of scored bookmarks
107
+ ##
108
+ def get_safari_bookmarks(parent, terms)
109
+ results = []
110
+
111
+ if parent.is_a?(Array)
112
+ parent.each do |c|
113
+ if c.is_a?(Hash)
114
+ if c.key?('Children')
115
+ results.concat(get_safari_bookmarks(c['Children'], terms))
116
+ elsif c.key?('URIDictionary')
117
+ title = c['URIDictionary']['title']
118
+ url = c['URLString']
119
+ scored = score_bookmark({ url: url, title: title }, terms)
120
+
121
+ results.push(scored) if scored[:score] > 7
122
+ end
123
+ end
124
+ end
125
+ elsif parent&.key?('Children')
126
+ results.concat(get_safari_bookmarks(parent['Children'], terms))
127
+ end
128
+
129
+ results.sort_by { |h| [h[:score], h[:title].length * -1] }.reverse
130
+ end
131
+ end
132
+ end
133
+ end