snibbets 2.0.29 → 2.0.31
Sign up to get free protection for your applications and to get access to all the features.
- 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
|