snibbets 2.0.29 → 2.0.31
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.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +11 -0
- data/.devcontainer/devcontainer.json +17 -0
- data/.editorconfig +9 -0
- data/.irbrc +2 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -58
- data/Gemfile.lock +1 -1
- data/README.md +4 -1
- data/Snibbets.lbaction/Contents/Info.plist +49 -0
- data/Snibbets.lbaction/Contents/Scripts/default.js +52 -0
- data/Snibbets.lbaction/Contents/Scripts/snibbets.rb +420 -0
- data/bin/snibbets +4 -0
- data/lib/snibbets/array.rb +5 -1
- data/lib/snibbets/config.rb +1 -0
- data/lib/snibbets/highlight.rb +17 -1
- data/lib/snibbets/menu.rb +4 -8
- data/lib/snibbets/os.rb +3 -3
- data/lib/snibbets/string.rb +78 -37
- data/lib/snibbets/todo_spec.rb +11 -0
- data/lib/snibbets/version.rb +1 -1
- data/lib/snibbets.rb +25 -19
- data/scripts/fixreadme.rb +23 -0
- data/snibbets.gemspec +3 -3
- data/{README.md.orig → src/_README.md} +56 -35
- data/yard_templates/default/method_details/setup.rb +3 -0
- metadata +15 -6
- data/Gemfile.lock.orig +0 -107
- data/buildnotes.md +0 -47
- data/snibbets.taskpaper +0 -14
@@ -0,0 +1,420 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Snibbets 2.0.0
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'readline'
|
6
|
+
require 'json'
|
7
|
+
require 'cgi'
|
8
|
+
require 'shellwords'
|
9
|
+
|
10
|
+
$search_path = File.expand_path("~/Desktop/Code/Snippets")
|
11
|
+
|
12
|
+
class String
|
13
|
+
# Are there multiple snippets (indicated by ATX headers)
|
14
|
+
def multiple?
|
15
|
+
return self.scan(/^#+/).length > 1
|
16
|
+
end
|
17
|
+
|
18
|
+
# Is the snippet in this block fenced?
|
19
|
+
def fenced?
|
20
|
+
count = self.scan(/^```/).length
|
21
|
+
return count > 1 && count.even?
|
22
|
+
end
|
23
|
+
|
24
|
+
def rx
|
25
|
+
return ".*" + self.gsub(/\s+/,'.*') + ".*"
|
26
|
+
end
|
27
|
+
|
28
|
+
# remove outside comments, fences, and indentation
|
29
|
+
def clean_code
|
30
|
+
block = self
|
31
|
+
|
32
|
+
# if it's a fenced code block, just discard the fence and everything
|
33
|
+
# outside it
|
34
|
+
if block.fenced?
|
35
|
+
return block.gsub(/(?:^|.*?\n)(`{3,})(\w+)?(.*?)\n\1.*/m) {|m| $3.strip }
|
36
|
+
end
|
37
|
+
|
38
|
+
# assume it's indented code, discard non-indented lines and outdent
|
39
|
+
# the rest
|
40
|
+
indent = nil
|
41
|
+
inblock = false
|
42
|
+
code = []
|
43
|
+
block.split(/\n/).each {|line|
|
44
|
+
if line =~ /^\s*$/ && inblock
|
45
|
+
code.push(line)
|
46
|
+
elsif line =~ /^( {4,}|\t+)/
|
47
|
+
inblock = true
|
48
|
+
indent ||= Regexp.new("^#{$1}")
|
49
|
+
code.push(line.sub(indent,''))
|
50
|
+
else
|
51
|
+
inblock = false
|
52
|
+
end
|
53
|
+
}
|
54
|
+
code.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns an array of snippets. Single snippets are returned without a
|
58
|
+
# title, multiple snippets get titles from header lines
|
59
|
+
def snippets
|
60
|
+
content = self.dup
|
61
|
+
# If there's only one snippet, just clean it and return
|
62
|
+
unless self.multiple?
|
63
|
+
return [{"title" => "", "code" => content.clean_code.strip}]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Split content by ATX headers. Everything on the line after the #
|
67
|
+
# becomes the title, code is gleaned from text between that and the
|
68
|
+
# next ATX header (or end)
|
69
|
+
sections = []
|
70
|
+
parts = content.split(/^#+/)
|
71
|
+
parts.shift
|
72
|
+
|
73
|
+
parts.each do |p|
|
74
|
+
lines = p.split(/\n/)
|
75
|
+
title = lines.shift.strip.sub(/[.:]$/, '')
|
76
|
+
block = lines.join("\n")
|
77
|
+
code = block.clean_code
|
78
|
+
next unless code && !code.empty?
|
79
|
+
|
80
|
+
sections << {
|
81
|
+
'title' => title,
|
82
|
+
'code' => code.strip
|
83
|
+
}
|
84
|
+
end
|
85
|
+
return sections
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def gum_menu(executable, res, title)
|
90
|
+
options = res.map { |m| m['title'] }
|
91
|
+
puts title
|
92
|
+
selection = `echo #{Shellwords.escape(options.join("\n"))} | #{executable} choose --limit 1`.strip
|
93
|
+
Process.exit 1 if selection.empty?
|
94
|
+
|
95
|
+
res.select { |m| m['title'] =~ /#{selection}/ }[0]
|
96
|
+
end
|
97
|
+
|
98
|
+
def fzf_menu(executable, res, title)
|
99
|
+
options = res.map { |m| m['title'] }
|
100
|
+
args = [
|
101
|
+
"--height=#{options.count + 2}",
|
102
|
+
%(--prompt="#{title} > "),
|
103
|
+
'-1'
|
104
|
+
]
|
105
|
+
selection = `echo #{Shellwords.escape(options.join("\n"))} | #{executable} #{args.join(' ')}`.strip
|
106
|
+
Process.exit 1 if selection.empty?
|
107
|
+
|
108
|
+
res.select { |m| m['title'] =~ /#{selection}/ }[0]
|
109
|
+
end
|
110
|
+
|
111
|
+
def menu(res, title = 'Select one')
|
112
|
+
fzf = `which fzf`.strip
|
113
|
+
return fzf_menu(fzf, res, title) unless fzf.empty?
|
114
|
+
|
115
|
+
gum = `which gum`.strip
|
116
|
+
return gum_menu(gum, res, title) unless gum.empty?
|
117
|
+
|
118
|
+
console_menu(res, title)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Generate a numbered menu, items passed must have a title property
|
122
|
+
def console_menu(res, title)
|
123
|
+
stty_save = `stty -g`.chomp
|
124
|
+
|
125
|
+
trap('INT') do
|
126
|
+
system('stty', stty_save)
|
127
|
+
Process.exit 1
|
128
|
+
end
|
129
|
+
|
130
|
+
# Generate a numbered menu, items passed must have a title property('INT') { system('stty', stty_save); exit }
|
131
|
+
counter = 1
|
132
|
+
$stderr.puts
|
133
|
+
res.each do |m|
|
134
|
+
$stderr.printf("%<counter>2d) %<title>s\n", counter: counter, title: m['title'])
|
135
|
+
counter += 1
|
136
|
+
end
|
137
|
+
$stderr.puts
|
138
|
+
|
139
|
+
begin
|
140
|
+
$stderr.printf(title.sub(/:?$/, ': '), res.length)
|
141
|
+
while (line = Readline.readline('', true))
|
142
|
+
unless line =~ /^[0-9]/
|
143
|
+
system('stty', stty_save) # Restore
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
line = line.to_i
|
147
|
+
return res[line - 1] if line.positive? && line <= res.length
|
148
|
+
|
149
|
+
warn 'Out of range'
|
150
|
+
menu(res, title)
|
151
|
+
end
|
152
|
+
rescue Interrupt
|
153
|
+
system('stty', stty_save)
|
154
|
+
exit
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Search the snippets directory for query using find and grep
|
159
|
+
def search(query, folder, try = 0)
|
160
|
+
# First try only search by filenames
|
161
|
+
# Second try search with grep
|
162
|
+
# Third try search with Spotlight name only
|
163
|
+
# Fourth try search with Spotlight all contents
|
164
|
+
cmd = case try
|
165
|
+
when 0
|
166
|
+
%(find "#{folder}" -iregex '#{query.rx}')
|
167
|
+
when 1
|
168
|
+
%(grep -iEl '#{query.rx}' "#{folder}/"*.md)
|
169
|
+
when 2
|
170
|
+
%(mdfind -onlyin "#{folder}" -name '#{query}' 2>/dev/null)
|
171
|
+
when 3
|
172
|
+
%(mdfind -onlyin "#{folder}" '#{query}' 2>/dev/null)
|
173
|
+
end
|
174
|
+
|
175
|
+
matches = `#{cmd}`.strip
|
176
|
+
|
177
|
+
results = []
|
178
|
+
|
179
|
+
if !matches.empty?
|
180
|
+
lines = matches.split(/\n/)
|
181
|
+
lines.each do |l|
|
182
|
+
results << {
|
183
|
+
'title' => File.basename(l, '.*'),
|
184
|
+
'path' => l
|
185
|
+
}
|
186
|
+
end
|
187
|
+
results
|
188
|
+
else
|
189
|
+
return [] if try > 2
|
190
|
+
|
191
|
+
# if no results on the first try, try again searching all text
|
192
|
+
search(query, folder, try + 1)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def highlight_pygments(executable, code, syntax, theme)
|
197
|
+
syntax = syntax.empty? ? '-g' : "-l #{syntax}"
|
198
|
+
`echo #{Shellwords.escape(code)} | #{executable} #{syntax}`
|
199
|
+
end
|
200
|
+
|
201
|
+
def highlight_skylight(executable, code, syntax, theme)
|
202
|
+
return code if syntax.empty?
|
203
|
+
|
204
|
+
`echo #{Shellwords.escape(code)} | #{executable} --syntax #{syntax}`
|
205
|
+
end
|
206
|
+
|
207
|
+
def highlight(code, filename, theme = 'monokai')
|
208
|
+
syntax = syntax_from_extension(filename)
|
209
|
+
|
210
|
+
skylight = `which skylighting`.strip
|
211
|
+
return highlight_skylight(skylight, code, syntax, theme) unless skylight.empty?
|
212
|
+
|
213
|
+
pygments = `which pygmentize`.strip
|
214
|
+
return highlight_pygments(pygments, code, syntax, theme) unless pygments.empty?
|
215
|
+
|
216
|
+
code
|
217
|
+
end
|
218
|
+
|
219
|
+
def ext_to_lang(ext)
|
220
|
+
case ext
|
221
|
+
when /^(as|applescript|scpt)$/
|
222
|
+
'applescript'
|
223
|
+
when /^m$/
|
224
|
+
'objective-c'
|
225
|
+
when /^(pl|perl)$/
|
226
|
+
'perl'
|
227
|
+
when /^py$/
|
228
|
+
'python'
|
229
|
+
when /^(js|jq(uery)?|jxa)$/
|
230
|
+
'javascript'
|
231
|
+
when /^rb$/
|
232
|
+
'ruby'
|
233
|
+
when /^cc$/
|
234
|
+
'c'
|
235
|
+
when /^(ba|fi|z|c)?sh$/
|
236
|
+
'bash'
|
237
|
+
when /^pl$/
|
238
|
+
'perl'
|
239
|
+
else
|
240
|
+
if %w[awk sed css sass scss less cpp php c sh swift html erb json xpath sql htaccess].include?(ext)
|
241
|
+
ext
|
242
|
+
else
|
243
|
+
''
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def syntax_from_extension(filename)
|
249
|
+
ext_to_lang(filename.split(/\./)[1])
|
250
|
+
end
|
251
|
+
|
252
|
+
options = {
|
253
|
+
interactive: true,
|
254
|
+
launchbar: false,
|
255
|
+
output: 'raw',
|
256
|
+
source: $search_path,
|
257
|
+
highlight: false,
|
258
|
+
all: false
|
259
|
+
}
|
260
|
+
|
261
|
+
optparse = OptionParser.new do |opts|
|
262
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options] query"
|
263
|
+
|
264
|
+
opts.on('-q', '--quiet', 'Skip menus and display first match') do
|
265
|
+
options[:interactive] = false
|
266
|
+
options[:launchbar] = false
|
267
|
+
end
|
268
|
+
|
269
|
+
opts.on('-a', '--all', 'If a file contains multiple snippets, output all of them (no menu)') do
|
270
|
+
options[:all] = true
|
271
|
+
end
|
272
|
+
|
273
|
+
opts.on('-o', '--output FORMAT', 'Output format (launchbar or raw)') do |outformat|
|
274
|
+
valid = %w[json launchbar lb raw]
|
275
|
+
if outformat.downcase =~ /(launchbar|lb)/
|
276
|
+
options[:launchbar] = true
|
277
|
+
options[:interactive] = false
|
278
|
+
elsif valid.include?(outformat.downcase)
|
279
|
+
options[:output] = outformat.downcase
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
opts.on('-s', '--source FOLDER', 'Snippets folder to search') do |folder|
|
284
|
+
options[:source] = File.expand_path(folder)
|
285
|
+
end
|
286
|
+
|
287
|
+
opts.on('--highlight', 'Use pygments or skylighting to syntax highlight (if installed)') do
|
288
|
+
options[:highlight] = true
|
289
|
+
end
|
290
|
+
|
291
|
+
opts.on('-h', '--help', 'Display this screen') do
|
292
|
+
puts optparse
|
293
|
+
Process.exit 0
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
optparse.parse!
|
298
|
+
|
299
|
+
query = ''
|
300
|
+
|
301
|
+
if options[:launchbar]
|
302
|
+
query = if $stdin.stat.size.positive?
|
303
|
+
$stdin.read.force_encoding('utf-8')
|
304
|
+
else
|
305
|
+
ARGV.join(' ')
|
306
|
+
end
|
307
|
+
elsif ARGV.length
|
308
|
+
query = ARGV.join(' ')
|
309
|
+
end
|
310
|
+
|
311
|
+
query = CGI.unescape(query)
|
312
|
+
|
313
|
+
if query.strip.empty?
|
314
|
+
puts 'No search query'
|
315
|
+
puts optparse
|
316
|
+
Process.exit 1
|
317
|
+
end
|
318
|
+
|
319
|
+
results = search(query, options[:source], 0)
|
320
|
+
|
321
|
+
if options[:launchbar]
|
322
|
+
output = []
|
323
|
+
|
324
|
+
if results.empty?
|
325
|
+
out = {
|
326
|
+
'title' => 'No matching snippets found'
|
327
|
+
}.to_json
|
328
|
+
puts out
|
329
|
+
Process.exit
|
330
|
+
end
|
331
|
+
|
332
|
+
results.each do |result|
|
333
|
+
input = IO.read(result['path'])
|
334
|
+
snippets = input.snippets
|
335
|
+
next if snippets.empty?
|
336
|
+
|
337
|
+
children = []
|
338
|
+
|
339
|
+
if snippets.length == 1
|
340
|
+
output << {
|
341
|
+
'title' => result['title'],
|
342
|
+
'path' => result['path'],
|
343
|
+
'action' => 'copyIt',
|
344
|
+
'actionArgument' => snippets[0]['code'],
|
345
|
+
'label' => 'Copy'
|
346
|
+
}
|
347
|
+
next
|
348
|
+
end
|
349
|
+
|
350
|
+
snippets.each { |s|
|
351
|
+
children << {
|
352
|
+
'title' => s['title'],
|
353
|
+
'path' => result['path'],
|
354
|
+
'action' => 'copyIt',
|
355
|
+
'actionArgument' => s['code'],
|
356
|
+
'label' => 'Copy'
|
357
|
+
}
|
358
|
+
}
|
359
|
+
|
360
|
+
output << {
|
361
|
+
'title' => result['title'],
|
362
|
+
'path' => result['path'],
|
363
|
+
'children' => children
|
364
|
+
}
|
365
|
+
end
|
366
|
+
|
367
|
+
puts output.to_json
|
368
|
+
else
|
369
|
+
filepath = nil
|
370
|
+
if results.empty?
|
371
|
+
warn 'No results'
|
372
|
+
Process.exit 0
|
373
|
+
elsif results.length == 1 || !options[:interactive]
|
374
|
+
filepath = results[0]['path']
|
375
|
+
input = IO.read(filepath)
|
376
|
+
else
|
377
|
+
answer = menu(results, 'Select a file')
|
378
|
+
filepath = answer['path']
|
379
|
+
input = IO.read(filepath)
|
380
|
+
end
|
381
|
+
|
382
|
+
snippets = input.snippets
|
383
|
+
|
384
|
+
if snippets.empty?
|
385
|
+
warn 'No snippets found'
|
386
|
+
Process.exit 0
|
387
|
+
elsif snippets.length == 1 || !options[:interactive]
|
388
|
+
if options[:output] == 'json'
|
389
|
+
$stdout.puts snippets.to_json
|
390
|
+
else
|
391
|
+
snippets.each do |snip|
|
392
|
+
code = snip['code']
|
393
|
+
code = highlight(code, filepath) if options[:highlight]
|
394
|
+
$stdout.puts code
|
395
|
+
end
|
396
|
+
end
|
397
|
+
elsif snippets.length > 1
|
398
|
+
if options[:all]
|
399
|
+
if options[:output] == 'json'
|
400
|
+
$stdout.puts snippets.to_json
|
401
|
+
else
|
402
|
+
snippets.each do |snippet|
|
403
|
+
$stdout.puts snippet['title']
|
404
|
+
$stdout.puts '------'
|
405
|
+
$stdout.puts snippet['code']
|
406
|
+
$stdout.puts
|
407
|
+
end
|
408
|
+
end
|
409
|
+
else
|
410
|
+
answer = menu(snippets, 'Select snippet')
|
411
|
+
if options[:output] == 'json'
|
412
|
+
$stdout.puts answer.to_json
|
413
|
+
else
|
414
|
+
code = answer['code']
|
415
|
+
code = highlight(code, filepath) if options[:highlight]
|
416
|
+
$stdout.puts code
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
data/bin/snibbets
CHANGED
@@ -46,6 +46,10 @@ module Snibbets
|
|
46
46
|
options[:name_only] = v
|
47
47
|
end
|
48
48
|
|
49
|
+
opts.on('--[no-]notes', 'Display the full content of the snippet') do |v|
|
50
|
+
options[:all_notes] = v
|
51
|
+
end
|
52
|
+
|
49
53
|
opts.on('-o', '--output FORMAT', 'Output format (json|launchbar|*raw)') do |outformat|
|
50
54
|
valid = %w[json launchbar lb raw]
|
51
55
|
if outformat.downcase =~ /(launchbar|lb)/
|
data/lib/snibbets/array.rb
CHANGED
@@ -4,6 +4,10 @@ module Snibbets
|
|
4
4
|
select { |el| el =~ /^<block\d+>$/ }.count
|
5
5
|
end
|
6
6
|
|
7
|
+
def notes
|
8
|
+
select { |el| el !~ /^<block\d+>$/ && el !~ /^```/ && !el.strip.empty? }.count
|
9
|
+
end
|
10
|
+
|
7
11
|
def strip_empty
|
8
12
|
remove_leading_empty_elements.remove_trailing_empty_elements
|
9
13
|
end
|
@@ -17,7 +21,7 @@ module Snibbets
|
|
17
21
|
|
18
22
|
in_leader = true
|
19
23
|
each do |line|
|
20
|
-
if (line
|
24
|
+
if (line.strip.empty?) && in_leader
|
21
25
|
next
|
22
26
|
else
|
23
27
|
in_leader = false
|
data/lib/snibbets/config.rb
CHANGED
data/lib/snibbets/highlight.rb
CHANGED
@@ -41,8 +41,24 @@ module Snibbets
|
|
41
41
|
# `echo #{Shellwords.escape(code)} | #{executable} #{theme}--syntax #{syntax}`
|
42
42
|
end
|
43
43
|
|
44
|
+
def highlight_fences(code, filename, syntax)
|
45
|
+
content = code.dup
|
46
|
+
|
47
|
+
content.fences.each do |f|
|
48
|
+
rx = Regexp.new(Regexp.escape(f[:code]))
|
49
|
+
highlighted = highlight(f[:code].gsub(/\\k</, '\k\<'), filename, f[:lang] || syntax).strip
|
50
|
+
code.sub!(/#{rx}/, highlighted)
|
51
|
+
end
|
52
|
+
|
53
|
+
Snibbets.options[:all_notes] ? code.gsub(/k\\</, 'k<') : code.gsub(/k\\</, 'k<').clean_code
|
54
|
+
end
|
55
|
+
|
44
56
|
def highlight(code, filename, syntax, theme = nil)
|
45
|
-
|
57
|
+
unless $stdout.isatty
|
58
|
+
return Snibbets.options[:all_notes] && code.replace_blocks[0].notes? ? code : code.clean_code
|
59
|
+
end
|
60
|
+
|
61
|
+
return highlight_fences(code, filename, syntax) if code.fenced?
|
46
62
|
|
47
63
|
theme ||= Snibbets.options[:highlight_theme] || 'monokai'
|
48
64
|
syntax ||= Lexers.syntax_from_extension(filename)
|
data/lib/snibbets/menu.rb
CHANGED
@@ -14,9 +14,7 @@ module Snibbets
|
|
14
14
|
def remove_items_without_query(filename, res, query)
|
15
15
|
q = find_query_in_options(filename, res, query).split(/ /)
|
16
16
|
res.delete_if do |opt|
|
17
|
-
q.none?
|
18
|
-
"#{filename} #{opt['title']}" =~ /#{word}/i
|
19
|
-
end
|
17
|
+
q.none? { |word| "#{filename} #{opt['title']}" =~ /#{word}/i }
|
20
18
|
end
|
21
19
|
res
|
22
20
|
end
|
@@ -28,16 +26,14 @@ module Snibbets
|
|
28
26
|
end
|
29
27
|
|
30
28
|
if res.count.zero?
|
31
|
-
warn 'No matches found'
|
29
|
+
warn 'No matches found' if Snibbets.options[:interactive]
|
32
30
|
Process.exit 1
|
33
31
|
end
|
34
32
|
|
35
33
|
options = res.map { |m| m['title'] }
|
36
34
|
|
37
35
|
puts title
|
38
|
-
args = [
|
39
|
-
"--height=#{options.count}"
|
40
|
-
]
|
36
|
+
args = ["--height=#{options.count}"]
|
41
37
|
selection = `echo #{Shellwords.escape(options.join("\n"))} | #{executable} filter #{args.join(' ')}`.strip
|
42
38
|
Process.exit 1 if selection.empty?
|
43
39
|
|
@@ -82,7 +78,7 @@ module Snibbets
|
|
82
78
|
end
|
83
79
|
|
84
80
|
if res.count.zero?
|
85
|
-
warn 'No matches found'
|
81
|
+
warn 'No matches found' if Snibbets.options[:interactive]
|
86
82
|
Process.exit 1
|
87
83
|
end
|
88
84
|
|
data/lib/snibbets/os.rb
CHANGED
@@ -31,12 +31,12 @@ module Snibbets
|
|
31
31
|
os = RbConfig::CONFIG['target_os']
|
32
32
|
case os
|
33
33
|
when /darwin.*/i
|
34
|
-
`pbpaste -pboard general -Prefer txt
|
34
|
+
`pbpaste -pboard general -Prefer txt`.strip_newlines
|
35
35
|
else
|
36
36
|
if TTY::Which.exist?('xclip')
|
37
|
-
`xclip -o -sel c
|
37
|
+
`xclip -o -sel c`.strip_newlines
|
38
38
|
elsif TTY::Which.exist('xsel')
|
39
|
-
`xsel -ob
|
39
|
+
`xsel -ob`.strip_newlines
|
40
40
|
else
|
41
41
|
puts 'Paste not supported on this system, please install xclip or xsel.'
|
42
42
|
end
|
data/lib/snibbets/string.rb
CHANGED
@@ -47,6 +47,28 @@ module Snibbets
|
|
47
47
|
count > 1 && count.even?
|
48
48
|
end
|
49
49
|
|
50
|
+
def blocks
|
51
|
+
replace_blocks[1].count
|
52
|
+
end
|
53
|
+
|
54
|
+
def blocks?
|
55
|
+
blocks.positive?
|
56
|
+
end
|
57
|
+
|
58
|
+
def notes?
|
59
|
+
replace_blocks[0].split("\n").notes.positive?
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return array of fenced code blocks
|
63
|
+
def fences
|
64
|
+
return [] unless fenced?
|
65
|
+
|
66
|
+
rx = /(?mi)^(?<fence>`{3,})(?<lang> *\S+)? *\n(?<code>[\s\S]*?)\n\k<fence> *(?=\n|\Z)/
|
67
|
+
matches = []
|
68
|
+
scan(rx) { matches << Regexp.last_match }
|
69
|
+
matches.each_with_object([]) { |m, fenced| fenced.push({ code: m['code'], lang: m['lang'] }) }
|
70
|
+
end
|
71
|
+
|
50
72
|
def indented?
|
51
73
|
self =~ /^( {4,}|\t+)/
|
52
74
|
end
|
@@ -90,11 +112,9 @@ module Snibbets
|
|
90
112
|
|
91
113
|
indent = code[0].match(/^( {4,}|\t+)(?=\S)/)
|
92
114
|
|
93
|
-
if indent
|
94
|
-
|
95
|
-
|
96
|
-
self
|
97
|
-
end
|
115
|
+
return self if indent.nil?
|
116
|
+
|
117
|
+
code.map! { |line| line.sub(/(?mi)^#{indent[1]}/, '') }.join("\n")
|
98
118
|
end
|
99
119
|
|
100
120
|
def replace_blocks
|
@@ -129,50 +149,44 @@ module Snibbets
|
|
129
149
|
[sans_blocks, code_blocks]
|
130
150
|
end
|
131
151
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
# return [{ 'title' => '', 'code' => content.clean_code.strip }] unless multiple?
|
138
|
-
|
139
|
-
# Split content by ATX headers. Everything on the line after the #
|
140
|
-
# becomes the title, code is gleaned from text between that and the
|
141
|
-
# next ATX header (or end)
|
142
|
-
sections = []
|
143
|
-
|
144
|
-
sans_blocks, code_blocks = content.replace_blocks
|
145
|
-
|
146
|
-
content = []
|
147
|
-
if sans_blocks =~ /<block\d+>/
|
148
|
-
sans_blocks.each_line do |line|
|
149
|
-
content << line if line =~ /^#/ || line =~ /<block\d+>/
|
150
|
-
end
|
151
|
-
|
152
|
-
parts = content.join("\n").split(/^#+/)
|
153
|
-
else
|
154
|
-
parts = sans_blocks.gsub(/\n{2,}/, "\n\n").split(/^#+/)
|
152
|
+
def parse_lang_marker(block)
|
153
|
+
lang = nil
|
154
|
+
if block =~ /<lang:(.*?)>/
|
155
|
+
lang = Regexp.last_match(1)
|
156
|
+
block = block.gsub(/<lang:.*?>\n+/, '').strip_empty
|
155
157
|
end
|
156
158
|
|
157
|
-
|
159
|
+
[lang, block]
|
160
|
+
end
|
161
|
+
|
162
|
+
def restore_blocks(parts, code_blocks)
|
163
|
+
sections = []
|
158
164
|
|
159
165
|
parts.each do |part|
|
160
166
|
lines = part.split(/\n/).strip_empty
|
161
|
-
next if lines.blocks == 0
|
162
167
|
|
163
|
-
|
164
|
-
|
168
|
+
notes = part.notes?
|
169
|
+
|
170
|
+
next if lines.blocks.zero? && !notes
|
171
|
+
|
172
|
+
title = if lines.count > 1 && lines[0] !~ /<block\d+>/ && lines[0] =~ /^ +/
|
173
|
+
lines.shift.strip.sub(/[.:]$/, '')
|
174
|
+
else
|
175
|
+
'Default snippet'
|
176
|
+
end
|
165
177
|
|
166
|
-
|
167
|
-
|
168
|
-
lang =
|
169
|
-
|
178
|
+
block = lines.join("\n").gsub(/<(block\d+)>/) do
|
179
|
+
code = code_blocks[Regexp.last_match(1)].strip_empty
|
180
|
+
lang, code = parse_lang_marker(code)
|
181
|
+
"\n```#{lang}\n#{code.strip}\n```"
|
170
182
|
end
|
171
183
|
|
172
|
-
code = block
|
184
|
+
lang, code = parse_lang_marker(block)
|
173
185
|
|
174
186
|
next unless code && !code.empty?
|
175
187
|
|
188
|
+
# code = code.clean_code unless notes || code.fences.count > 1
|
189
|
+
|
176
190
|
sections << {
|
177
191
|
'title' => title,
|
178
192
|
'code' => code.strip_empty,
|
@@ -182,5 +196,32 @@ module Snibbets
|
|
182
196
|
|
183
197
|
sections
|
184
198
|
end
|
199
|
+
|
200
|
+
# Returns an array of snippets. Single snippets are returned without a
|
201
|
+
# title, multiple snippets get titles from header lines
|
202
|
+
def snippets
|
203
|
+
content = dup.remove_meta
|
204
|
+
# If there's only one snippet, just clean it and return
|
205
|
+
# return [{ 'title' => '', 'code' => content.clean_code.strip }] unless multiple?
|
206
|
+
|
207
|
+
# Split content by ATX headers. Everything on the line after the #
|
208
|
+
# becomes the title, code is gleaned from text between that and the
|
209
|
+
# next ATX header (or end)
|
210
|
+
sans_blocks, code_blocks = content.replace_blocks
|
211
|
+
|
212
|
+
parts = if Snibbets.options[:all_notes]
|
213
|
+
sans_blocks.split(/^#+/)
|
214
|
+
elsif sans_blocks =~ /<block\d+>/
|
215
|
+
sans_blocks.split(/\n/).each_with_object([]) do |line, arr|
|
216
|
+
arr << line if line =~ /^#/ || line =~ /<block\d+>/
|
217
|
+
end.join("\n").split(/^#+/)
|
218
|
+
else
|
219
|
+
sans_blocks.gsub(/\n{2,}/, "\n\n").split(/^#+/)
|
220
|
+
end
|
221
|
+
|
222
|
+
# parts.shift if parts.count > 1
|
223
|
+
|
224
|
+
restore_blocks(parts, code_blocks)
|
225
|
+
end
|
185
226
|
end
|
186
227
|
end
|