howzit 1.2.13 → 1.2.16

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.
@@ -0,0 +1,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howzit
4
+ # BuildNote Class
5
+ class BuildNote
6
+ attr_accessor :topics
7
+
8
+ attr_reader :metadata, :title
9
+
10
+ def initialize(file: nil, args: [])
11
+ @topics = []
12
+ @metadata = {}
13
+
14
+ read_help(file)
15
+ end
16
+
17
+ def inspect
18
+ puts "#<Howzit::BuildNote @topics=[#{@topics.count}]>"
19
+ end
20
+
21
+ def run
22
+ process
23
+ end
24
+
25
+ def edit
26
+ edit_note
27
+ end
28
+
29
+ def find_topic(term)
30
+ @topics.filter do |topic|
31
+ rx = term.to_rx
32
+ topic.title.downcase =~ rx
33
+ end
34
+ end
35
+
36
+ def grep(term)
37
+ @topics.filter { |topic| topic.grep(term) }
38
+ end
39
+
40
+ # Output a list of topic titles
41
+ def list
42
+ output = []
43
+ output.push(Color.template("{bg}Topics:{x}\n"))
44
+ @topics.each do |topic|
45
+ output.push(Color.template("- {bw}#{topic.title}{x}"))
46
+ end
47
+ output.join("\n")
48
+ end
49
+
50
+ def list_topics
51
+ @topics.map { |topic| topic.title }
52
+ end
53
+
54
+ def list_completions
55
+ list_topics.join("\n")
56
+ end
57
+
58
+ def list_runnable_completions
59
+ output = []
60
+ @topics.each do |topic|
61
+ output.push(topic.title) if topic.tasks.count.positive?
62
+ end
63
+ output.join("\n")
64
+ end
65
+
66
+ def list_runnable
67
+ output = []
68
+ output.push(Color.template(%({bg}"Runnable" Topics:{x}\n)))
69
+ @topics.each do |topic|
70
+ s_out = []
71
+
72
+ topic.tasks.each do |task|
73
+ s_out.push(task.to_list)
74
+ end
75
+
76
+ unless s_out.empty?
77
+ output.push(Color.template("- {bw}#{topic.title}{x}"))
78
+ output.push(s_out.join("\n"))
79
+ end
80
+ end
81
+ output.join("\n")
82
+ end
83
+
84
+ def read_file(file)
85
+ read_help_file(file)
86
+ end
87
+
88
+ # Create a buildnotes skeleton
89
+ def create_note
90
+ trap('SIGINT') do
91
+ warn "\nCanceled"
92
+ exit!
93
+ end
94
+ default = !$stdout.isatty || Howzit.options[:default]
95
+ # First make sure there isn't already a buildnotes file
96
+ if note_file
97
+ fname = Color.template("{by}#{note_file}{bw}")
98
+ unless default
99
+ res = Prompt.yn("#{fname} exists and appears to be a build note, continue anyway?", false)
100
+ unless res
101
+ puts 'Canceled'
102
+ Process.exit 0
103
+ end
104
+ end
105
+ end
106
+
107
+ title = File.basename(Dir.pwd)
108
+ if default
109
+ input = title
110
+ else
111
+ printf Color.template("{bw}Project name {xg}[#{title}]{bw}: {x}")
112
+ input = $stdin.gets.chomp
113
+ title = input unless input.empty?
114
+ end
115
+ summary = ''
116
+ unless default
117
+ printf Color.template('{bw}Project summary: {x}')
118
+ input = $stdin.gets.chomp
119
+ summary = input unless input.empty?
120
+ end
121
+
122
+ fname = 'buildnotes.md'
123
+ unless default
124
+ printf Color.template("{bw}Build notes filename (must begin with 'howzit' or 'build')\n{xg}[#{fname}]{bw}: {x}")
125
+ input = $stdin.gets.chomp
126
+ fname = input unless input.empty?
127
+ end
128
+
129
+ note = <<~EOBUILDNOTES
130
+ # #{title}
131
+
132
+ #{summary}
133
+
134
+ ## File Structure
135
+
136
+ Where are the main editable files? Is there a dist/build folder that should be ignored?
137
+
138
+ ## Build
139
+
140
+ What build system/parameters does this use?
141
+
142
+ @run(./build command)
143
+
144
+ ## Deploy
145
+
146
+ What are the procedures/commands to deploy this project?
147
+
148
+ ## Other
149
+
150
+ Version control notes, additional gulp/rake/make/etc tasks...
151
+
152
+ EOBUILDNOTES
153
+
154
+ if File.exist?(fname) && !default
155
+ file = Color.template("{by}#{fname}")
156
+ res = Prompt.yn("Are you absolutely sure you want to overwrite #{file}", false)
157
+
158
+ unless res
159
+ puts 'Canceled'
160
+ Process.exit 0
161
+ end
162
+ end
163
+
164
+ File.open(fname, 'w') do |f|
165
+ f.puts note
166
+ puts Color.template("{by}Build notes for #{title} written to #{fname}")
167
+ end
168
+ end
169
+
170
+ def note_file
171
+ @note_file ||= find_note_file
172
+ end
173
+
174
+ private
175
+
176
+ ##
177
+ ## Traverse up directory tree looking for build notes
178
+ ##
179
+ ## @return topics dictionary
180
+ ##
181
+ def glob_upstream
182
+ home = Dir.pwd
183
+ dir = File.dirname(home)
184
+ buildnotes = []
185
+ filename = nil
186
+
187
+ while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
188
+ Dir.chdir(dir)
189
+ filename = glob_note
190
+ unless filename.nil?
191
+ note = File.join(dir, filename)
192
+ buildnotes.push(note) unless note == note_file
193
+ end
194
+ dir = File.dirname(dir)
195
+ end
196
+
197
+ Dir.chdir(home)
198
+
199
+ buildnotes.reverse
200
+ end
201
+
202
+ def is_build_notes(filename)
203
+ return false if filename.downcase !~ /(^howzit[^.]*|build[^.]+)/
204
+ return false if Howzit.config.should_ignore(filename)
205
+ true
206
+ end
207
+
208
+ def glob_note
209
+ filename = nil
210
+ # Check for a build note file in the current folder. Filename must start
211
+ # with "build" and have an extension of txt, md, or markdown.
212
+
213
+ Dir.glob('*.{txt,md,markdown}').each do |f|
214
+ if is_build_notes(f)
215
+ filename = f
216
+ break
217
+ end
218
+ end
219
+ filename
220
+ end
221
+
222
+ def find_note_file
223
+ filename = glob_note
224
+
225
+ if filename.nil? && 'git'.available?
226
+ proj_dir = `git rev-parse --show-toplevel 2>/dev/null`.strip
227
+ unless proj_dir == ''
228
+ Dir.chdir(proj_dir)
229
+ filename = glob_note
230
+ end
231
+ end
232
+
233
+ if filename.nil? && Howzit.options[:include_upstream]
234
+ upstream_notes = glob_upstream
235
+ filename = upstream_notes[-1] unless upstream_notes.empty?
236
+ end
237
+
238
+ return nil if filename.nil?
239
+
240
+ File.expand_path(filename)
241
+ end
242
+
243
+ def read_upstream
244
+ buildnotes = glob_upstream
245
+
246
+ topics_dict = []
247
+ buildnotes.each do |path|
248
+ topics_dict.concat(read_help_file(path))
249
+ end
250
+ topics_dict
251
+ end
252
+
253
+ def ensure_requirements(template)
254
+ t_leader = IO.read(template).split(/^#/)[0].strip
255
+ if t_leader.length > 0
256
+ t_meta = t_leader.get_metadata
257
+ if t_meta.key?('required')
258
+ required = t_meta['required'].strip.split(/\s*,\s*/)
259
+ required.each do |req|
260
+ unless @metadata.keys.include?(req.downcase)
261
+ warn Color.template(%({xr}ERROR: Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}))
262
+ warn Color.template(%({xr}Please define {by}#{req.downcase}{xr} in build notes{x}))
263
+ Process.exit 1
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ def get_template_topics(content)
271
+ leader = content.split(/^#/)[0].strip
272
+
273
+ template_topics = []
274
+
275
+ if leader.length > 0
276
+ data = leader.get_metadata
277
+ @metadata = @metadata.merge(data)
278
+
279
+ if data.key?('template')
280
+ templates = data['template'].strip.split(/\s*,\s*/)
281
+ templates.each do |t|
282
+ tasks = nil
283
+ if t =~ /\[(.*?)\]$/
284
+ tasks = Regexp.last_match[1].split(/\s*,\s*/).map {|t| t.gsub(/\*/, '.*?')}
285
+ t = t.sub(/\[.*?\]$/, '').strip
286
+ end
287
+
288
+ t_file = t.sub(/(\.md)?$/, '.md')
289
+ template = File.join(Howzit.config.template_folder, t_file)
290
+ if File.exist?(template)
291
+ ensure_requirements(template)
292
+
293
+ t_topics = BuildNote.new(file: template)
294
+ if tasks
295
+ tasks.each do |task|
296
+ t_topics.topics.each do |topic|
297
+ if topic.title =~ /^(.*?:)?#{task}$/i
298
+ topic.parent = t
299
+ template_topics.push(topic)
300
+ end
301
+ end
302
+ end
303
+ else
304
+ t_topics.topics.map! do |topic|
305
+ topic.parent = t
306
+ topic
307
+ end
308
+
309
+ template_topics.concat(t_topics.topics)
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end
315
+ template_topics
316
+ end
317
+
318
+ def include_file(m)
319
+ file = File.expand_path(m[1])
320
+
321
+ return m[0] unless File.exist?(file)
322
+
323
+ content = IO.read(file)
324
+ home = ENV['HOME']
325
+ short_path = File.dirname(file.sub(/^#{home}/, '~'))
326
+ prefix = "#{short_path}/#{File.basename(file)}:"
327
+ parts = content.split(/^##+/)
328
+ parts.shift
329
+ if parts.empty?
330
+ content
331
+ else
332
+ "## #{parts.join('## ')}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}")
333
+ end
334
+ end
335
+
336
+ def note_title(truncate = 0)
337
+ help = IO.read(note_file).strip
338
+ title = help.match(/(?:^(\S.*?)(?=\n==)|^# ?(.*?)$)/)
339
+ title = if title
340
+ title[1].nil? ? title[2] : title[1]
341
+ else
342
+ note_file.sub(/(\.\w+)?$/, '')
343
+ end
344
+
345
+ title && truncate.positive? ? title.trunc(truncate) : title
346
+ end
347
+
348
+ # Read in the build notes file and output a hash of "Title" => contents
349
+ def read_help_file(path = nil)
350
+ topics = []
351
+
352
+ filename = path.nil? ? note_file : path
353
+
354
+ help = IO.read(filename)
355
+
356
+ @title = note_title
357
+
358
+ help.gsub!(/@include\((.*?)\)/) do
359
+ include_file(Regexp.last_match)
360
+ end
361
+
362
+ template_topics = get_template_topics(help)
363
+
364
+ split = help.split(/^##+/)
365
+ split.slice!(0)
366
+ split.each do |sect|
367
+ next if sect.strip.empty?
368
+
369
+ lines = sect.split(/\n/)
370
+ title = lines.slice!(0).strip
371
+ prefix = ''
372
+ if path
373
+ if path =~ /#{Howzit.config.template_folder}/
374
+ short_path = File.basename(path, '.md')
375
+ else
376
+ home = ENV['HOME']
377
+ short_path = File.dirname(path.sub(/^#{home}/, '~'))
378
+ prefix = "_from #{short_path}_\n\n"
379
+ end
380
+ title = "#{short_path}:#{title}"
381
+ end
382
+ topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata))
383
+ topics.push(topic)
384
+ end
385
+
386
+ template_topics.each do |topic|
387
+ topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
388
+ end
389
+
390
+ topics
391
+ end
392
+
393
+ def read_help(path = nil)
394
+ @topics = read_help_file(path)
395
+ return unless path.nil? && Howzit.options[:include_upstream]
396
+
397
+ upstream_topics = read_upstream
398
+
399
+ upstream_topics.each do |topic|
400
+ @topics.push(topic) unless find_topic(title.sub(/^.+:/, '')).count.positive?
401
+ end
402
+ end
403
+
404
+ def edit_note
405
+ editor = Howzit.options.fetch(:editor, ENV['EDITOR'])
406
+
407
+ raise 'No editor defined' if editor.nil?
408
+
409
+ raise "Invalid editor (#{editor})" unless Util.valid_command?(editor)
410
+
411
+ if note_file.nil?
412
+ res = Prompt.yn('No build notes file found, create one?', true)
413
+
414
+ create_note if res
415
+ edit_note
416
+ else
417
+ `#{editor} "#{note_file}"`
418
+ end
419
+ end
420
+
421
+ def process_topic(topic, run, single = false)
422
+ new_topic = topic.dup
423
+
424
+ # Handle variable replacement
425
+
426
+ unless Howzit.arguments.empty?
427
+ new_topic.content = new_topic.content.gsub(/\$(\d+)/) do |m|
428
+ idx = m[1].to_i - 1
429
+ Howzit.arguments.length > idx ? Howzit.arguments[idx] : m
430
+ end
431
+ new_topic.content = new_topic.content.gsub(/\$[@*]/, Shellwords.join(Howzit.arguments))
432
+ end
433
+
434
+ output = if run
435
+ new_topic.run
436
+ else
437
+ new_topic.print_out({ single: single })
438
+ end
439
+ output.nil? ? '' : output.join("\n")
440
+ end
441
+
442
+ def process
443
+ output = []
444
+
445
+ unless note_file
446
+ Process.exit 0 if Howzit.options[:list_runnable_titles] || Howzit.options[:list_topic_titles]
447
+
448
+ # clear the buffer
449
+ ARGV.length.times do
450
+ ARGV.shift
451
+ end
452
+ res = yn("No build notes file found, create one?", true)
453
+ create_note if res
454
+ Process.exit 1
455
+ end
456
+
457
+ if Howzit.options[:title_only]
458
+ out = note_title(20)
459
+ $stdout.print(out.strip)
460
+ Process.exit(0)
461
+ elsif Howzit.options[:output_title] && !Howzit.options[:run]
462
+ if @title && !@title.empty?
463
+ header = @title.format_header({ hr: "\u{2550}", color: '{bwK}' })
464
+ output.push("#{header}\n")
465
+ end
466
+ end
467
+
468
+ if Howzit.options[:list_runnable]
469
+ if Howzit.options[:list_runnable_titles]
470
+ out = list_runnable_completions
471
+ $stdout.print(out.strip)
472
+ else
473
+ out = list_runnable
474
+ Util.show(out, { color: Howzit.options[:color], paginate: false, highlight: false })
475
+ end
476
+ Process.exit(0)
477
+ end
478
+
479
+ if Howzit.options[:list_topics]
480
+ if Howzit.options[:list_topic_titles]
481
+ $stdout.print(list_completions)
482
+ else
483
+ out = list
484
+ Util.show(out, { color: Howzit.options[:color], paginate: false, highlight: false })
485
+ end
486
+ Process.exit(0)
487
+ end
488
+
489
+ topic_matches = []
490
+ if Howzit.options[:grep]
491
+ matches = grep_topics(Howzit.options[:grep])
492
+ case Howzit.options[:multiple_matches]
493
+ when :all
494
+ topic_matches.concat(matches.sort)
495
+ else
496
+ topic_matches.concat(Prompt.choose(matches))
497
+ end
498
+ elsif Howzit.options[:choose]
499
+ titles = Prompt.choose(list_topics)
500
+ titles.each { |title| topic_matches.push(find_topic(title)[0]) }
501
+ # If there are arguments use those to search for a matching topic
502
+ elsif !Howzit.cli_args.empty?
503
+ search = Howzit.cli_args.join(' ').strip.downcase.split(/ *, */).map(&:strip)
504
+
505
+ search.each do |s|
506
+ matches = find_topic(s)
507
+
508
+ if matches.empty?
509
+ output.push(Color.template(%({bR}ERROR:{xr} No topic match found for {bw}#{s}{x}\n)))
510
+ else
511
+ case Howzit.options[:multiple_matches]
512
+ when :first
513
+ topic_matches.push(matches[0])
514
+ when :best
515
+ topic_matches.push(matches.sort.min_by { |t| t.title.length })
516
+ when :all
517
+ topic_matches.concat(matches)
518
+ else
519
+ titles = matches.map { |topic| topic.title }
520
+ res = Prompt.choose(titles)
521
+ res.each { |title| topic_matches.concat(find_topic(title)) }
522
+ end
523
+ end
524
+ end
525
+
526
+ if topic_matches.empty? && !Howzit.options[:show_all_on_error]
527
+ Util.show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
528
+ Process.exit 1
529
+ end
530
+ end
531
+
532
+ if !topic_matches.empty?
533
+ # If we found a match
534
+ topic_matches.each { |topic_match| output.push(process_topic(topic_match, Howzit.options[:run], true)) }
535
+ else
536
+ # If there's no argument or no match found, output all
537
+ topics.each { |k| output.push(process_topic(k, false, false)) }
538
+ end
539
+ Howzit.options[:paginate] = false if Howzit.options[:run]
540
+ Util.show(output.join("\n").strip, Howzit.options)
541
+ end
542
+ end
543
+ end