markdown_exec 1.3.7 → 1.3.9

Sign up to get free protection for your applications and to get access to all the features.
data/lib/markdown_exec.rb CHANGED
@@ -37,6 +37,13 @@ $stdout.sync = true
37
37
 
38
38
  BLOCK_SIZE = 1024
39
39
 
40
+ # macros
41
+ #
42
+ BACK_OPTION = '* Back'
43
+ EXIT_OPTION = '* Exit'
44
+ LOAD_FILE = true
45
+ VN = 'MDE_MENU_HISTORY'
46
+
40
47
  # custom error: file specified is missing
41
48
  #
42
49
  class FileMissingError < StandardError; end
@@ -58,6 +65,18 @@ class Hash
58
65
  end
59
66
  end
60
67
 
68
+ # integer value for comparison
69
+ #
70
+ def options_fetch_display_level(options)
71
+ options.fetch(:display_level, 1)
72
+ end
73
+
74
+ # integer value for comparison
75
+ #
76
+ def options_fetch_display_level_xbase_prefix(options)
77
+ options.fetch(:level_xbase_prefix, '')
78
+ end
79
+
61
80
  # stdout manager
62
81
  #
63
82
  module FOUT
@@ -77,7 +96,7 @@ module FOUT
77
96
  end
78
97
 
79
98
  def approved_fout?(level)
80
- level <= @options[:display_level]
99
+ level <= options_fetch_display_level(@options)
81
100
  end
82
101
 
83
102
  # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
@@ -85,7 +104,7 @@ module FOUT
85
104
  def lout(str, level: DISPLAY_LEVEL_BASE)
86
105
  return unless approved_fout? level
87
106
 
88
- fout level == DISPLAY_LEVEL_BASE ? str : @options[:display_level_xbase_prefix] + str
107
+ fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
89
108
  end
90
109
  end
91
110
 
@@ -121,6 +140,13 @@ module MarkdownExec
121
140
  FNR11 = '/'
122
141
  FNR12 = ',~'
123
142
 
143
+ SHELL_COLOR_OPTIONS = {
144
+ 'bash' => :menu_bash_color,
145
+ BLOCK_TYPE_LINK => :menu_link_color,
146
+ 'opts' => :menu_opts_color,
147
+ 'vars' => :menu_vars_color
148
+ }.freeze
149
+
124
150
  ##
125
151
  #
126
152
  # rubocop:disable Layout/LineLength
@@ -162,17 +188,6 @@ module MarkdownExec
162
188
  end
163
189
  end
164
190
 
165
- # return arguments after `--`
166
- #
167
- def arguments_for_child(argv = ARGV)
168
- case ind = argv.find_index('--')
169
- when nil, argv.count - 1
170
- []
171
- else
172
- argv[ind + 1..-1]
173
- end
174
- end
175
-
176
191
  ##
177
192
  # options necessary to start, parse input, defaults for cli options
178
193
  #
@@ -199,115 +214,179 @@ module MarkdownExec
199
214
  }
200
215
  end
201
216
 
217
+ # Execute a code block after approval and provide user interaction options.
218
+ #
219
+ # This method displays required code blocks, asks for user approval, and
220
+ # executes the code block if approved. It also allows users to copy the
221
+ # code to the clipboard or save it to a file.
222
+ #
223
+ # @param opts [Hash] Options hash containing configuration settings.
224
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
225
+ # @return [String] The name of the executed code block.
226
+ #
202
227
  def approve_and_execute_block(opts, mdoc)
203
- required_blocks = mdoc.collect_recursively_required_code(opts[:block_name])
204
- if opts[:output_script] || opts[:user_must_approve]
205
- display_required_code(opts,
206
- required_blocks)
228
+ selected = mdoc.get_block_by_name(opts[:block_name])
229
+ if selected[:shell] == BLOCK_TYPE_LINK
230
+ handle_link_shell(opts, selected)
231
+ elsif selected[:shell] == 'opts'
232
+ handle_opts_shell(opts, selected)
233
+ else
234
+ required_lines = collect_required_code_blocks(opts, mdoc, selected)
235
+ # Display required code blocks if requested or required approval.
236
+ if opts[:output_script] || opts[:user_must_approve]
237
+ display_required_code(opts, required_lines)
238
+ end
239
+
240
+ allow = true
241
+ allow = user_approval(opts, required_lines) if opts[:user_must_approve]
242
+ opts[:ir_approve] = allow
243
+ mdoc.get_block_by_name(opts[:block_name])
244
+ execute_approved_block(opts, required_lines) if opts[:ir_approve]
245
+
246
+ [!LOAD_FILE, '']
207
247
  end
248
+ end
208
249
 
209
- allow = true
210
- if opts[:user_must_approve]
211
- loop do
212
- (sel = @prompt.select(opts[:prompt_approve_block],
213
- filter: true) do |menu|
214
- menu.default 1
215
- menu.choice opts[:prompt_yes], 1
216
- menu.choice opts[:prompt_no], 2
217
- menu.choice opts[:prompt_script_to_clipboard], 3
218
- menu.choice opts[:prompt_save_script], 4
219
- end)
220
- allow = (sel == 1)
221
- if sel == 3
222
- text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
223
- Clipboard.copy(text)
224
- fout "Clipboard updated: #{required_blocks.count} blocks," /
225
- " #{required_blocks.flatten.count} lines," /
226
- " #{text.length} characters"
227
- end
228
- if sel == 4
229
- write_command_file(opts.merge(save_executed_script: true),
230
- required_blocks)
231
- fout "File saved: #{@options[:saved_filespec]}"
232
- end
233
- break if [1, 2].include? sel
234
- end
250
+ def handle_link_shell(opts, selected)
251
+ data = YAML.load(selected[:body].join("\n"))
252
+
253
+ # add to front of history
254
+ #
255
+ ENV[VN] = opts[:filename] + opts[:history_document_separator] + ENV.fetch(VN, '')
256
+
257
+ opts[:filename] = data.fetch('file', nil)
258
+ return !LOAD_FILE unless opts[:filename]
259
+
260
+ data.fetch('vars', []).each do |var|
261
+ ENV[var[0]] = var[1].to_s
235
262
  end
236
- (opts[:ir_approve] = allow)
237
263
 
238
- selected = mdoc.get_block_by_name opts[:block_name]
264
+ [LOAD_FILE, data.fetch('block', '')]
265
+ end
239
266
 
240
- if opts[:ir_approve]
241
- write_command_file opts, required_blocks
242
- command_execute opts, required_blocks.flatten.join("\n")
243
- save_execution_output
244
- output_execution_summary
245
- output_execution_result
267
+ def handle_opts_shell(opts, selected)
268
+ data = YAML.load(selected[:body].join("\n"))
269
+ data.each_key do |key|
270
+ opts[key.to_sym] = value = data[key].to_s
271
+ next unless opts[:menu_opts_set_format].present?
272
+
273
+ print format(
274
+ opts[:menu_opts_set_format],
275
+ { key: key,
276
+ value: value }
277
+ ).send(opts[:menu_opts_set_color].to_sym)
278
+ end
279
+ [!LOAD_FILE, '']
280
+ end
281
+
282
+ def user_approval(opts, required_lines)
283
+ # Present a selection menu for user approval.
284
+ sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
285
+ menu.default 1
286
+ menu.choice opts[:prompt_yes], 1
287
+ menu.choice opts[:prompt_no], 2
288
+ menu.choice opts[:prompt_script_to_clipboard], 3
289
+ menu.choice opts[:prompt_save_script], 4
290
+ end
291
+
292
+ if sel == 3
293
+ copy_to_clipboard(required_lines)
294
+ elsif sel == 4
295
+ save_to_file(opts, required_lines)
246
296
  end
247
297
 
248
- selected[:name]
298
+ sel == 1
299
+ end
300
+
301
+ def execute_approved_block(opts, required_lines)
302
+ write_command_file(opts, required_lines)
303
+ command_execute(
304
+ opts,
305
+ required_lines.flatten.join("\n"),
306
+ args: opts.fetch(:pass_args, [])
307
+ )
308
+ save_execution_output
309
+ output_execution_summary
310
+ output_execution_result
311
+ end
312
+
313
+ # Collect required code blocks based on the provided options.
314
+ #
315
+ # @param opts [Hash] Options hash containing configuration settings.
316
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
317
+ # @return [Array<String>] Required code blocks as an array of lines.
318
+ def collect_required_code_blocks(opts, mdoc, selected)
319
+ required = mdoc.collect_recursively_required_code(opts[:block_name])
320
+ required_lines = required[:code]
321
+ required[:blocks]
322
+
323
+ # Apply hash in opts block to environment variables
324
+ if selected[:shell] == BLOCK_TYPE_VARS
325
+ data = YAML.load(selected[:body].join("\n"))
326
+ data.each_key do |key|
327
+ ENV[key] = value = data[key].to_s
328
+ next unless opts[:menu_vars_set_format].present?
329
+
330
+ print format(
331
+ opts[:menu_vars_set_format],
332
+ { key: key,
333
+ value: value }
334
+ ).send(opts[:menu_vars_set_color].to_sym)
335
+ end
336
+ end
337
+
338
+ required_lines
249
339
  end
250
340
 
251
341
  def cfile
252
342
  @cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern))
253
343
  end
254
344
 
255
- # :reek:DuplicateMethodCall
256
- # :reek:UncommunicativeVariableName { exclude: [ e ] }
257
- # :reek:LongYieldList
258
- def command_execute(opts, command)
259
- #d 'execute command and yield outputs'
345
+ EF_STDOUT = :stdout
346
+ EF_STDERR = :stderr
347
+ EF_STDIN = :stdin
348
+
349
+ # Handles reading and processing lines from a given IO stream
350
+ #
351
+ # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
352
+ # @param file_type [Symbol] The type of file to which the stream corresponds.
353
+ def handle_stream(opts, stream, file_type, swap: false)
354
+ Thread.new do
355
+ until (line = stream.gets).nil?
356
+ @execute_files[file_type] = @execute_files[file_type] + [line.strip]
357
+ print line if opts[:output_stdout]
358
+ yield line if block_given?
359
+ end
360
+ rescue IOError
361
+ #d 'stdout IOError, thread killed, do nothing'
362
+ end
363
+ end
364
+
365
+ # Existing command_execute method
366
+ def command_execute(opts, command, args: [])
260
367
  @execute_files = Hash.new([])
261
368
  @execute_options = opts
262
369
  @execute_started_at = Time.now.utc
263
370
 
264
- args = []
265
- Open3.popen3(@options[:shell], '-c',
266
- command, ARGV[0], *args) do |stdin, stdout, stderr, exec_thr|
267
- #d 'command started'
268
- Thread.new do
269
- until (line = stdout.gets).nil?
270
- @execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
271
- print line if opts[:output_stdout]
272
- yield nil, line, nil, exec_thr if block_given?
273
- end
274
- rescue IOError
275
- #d 'stdout IOError, thread killed, do nothing'
371
+ Open3.popen3(opts[:shell], '-c', command, opts[:filename],
372
+ *args) do |stdin, stdout, stderr, exec_thr|
373
+ handle_stream(opts, stdout, EF_STDOUT) do |line|
374
+ yield nil, line, nil, exec_thr if block_given?
276
375
  end
277
-
278
- Thread.new do
279
- until (line = stderr.gets).nil?
280
- @execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
281
- print line if opts[:output_stdout]
282
- yield nil, nil, line, exec_thr if block_given?
283
- end
284
- rescue IOError
285
- #d 'stderr IOError, thread killed, do nothing'
376
+ handle_stream(opts, stderr, EF_STDERR) do |line|
377
+ yield nil, nil, line, exec_thr if block_given?
286
378
  end
287
379
 
288
- in_thr = Thread.new do
289
- while exec_thr.alive? # reading input until the child process ends
290
- stdin.puts(line = $stdin.gets)
291
- @execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
292
- yield line, nil, nil, exec_thr if block_given?
293
- end
294
- #d 'exec_thr now dead'
295
- rescue StandardError
296
- #d 'stdin error, thread killed, do nothing'
380
+ in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
381
+ stdin.puts(line)
382
+ yield line, nil, nil, exec_thr if block_given?
297
383
  end
298
384
 
299
- #d 'join exec_thr'
300
385
  exec_thr.join
301
-
302
- #d 'wait before closing stdin'
303
386
  sleep 0.1
304
-
305
- #d 'kill stdin thread'
306
- in_thr.kill
307
- # @return_code = exec_thr.value
308
- #d 'command end'
387
+ in_thr.kill if in_thr&.alive?
309
388
  end
310
- #d 'command completed'
389
+
311
390
  @execute_completed_at = Time.now.utc
312
391
  rescue Errno::ENOENT => err
313
392
  #d 'command error ENOENT triggered by missing command in script'
@@ -326,19 +405,19 @@ module MarkdownExec
326
405
  end
327
406
 
328
407
  def count_blocks_in_filename
329
- fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match]
408
+ fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
330
409
  cnt = 0
331
410
  cfile.readlines(@options[:filename]).each do |line|
332
- cnt += 1 if line.match(fenced_start_and_end_match)
411
+ cnt += 1 if line.match(fenced_start_and_end_regex)
333
412
  end
334
413
  cnt / 2
335
414
  end
336
415
 
337
416
  # :reek:DuplicateMethodCall
338
- def display_required_code(opts, required_blocks)
417
+ def display_required_code(opts, required_lines)
339
418
  frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
340
419
  fout frame
341
- required_blocks.each { |cb| fout cb }
420
+ required_lines.each { |cb| fout cb }
342
421
  fout frame
343
422
  end
344
423
 
@@ -408,7 +487,6 @@ module MarkdownExec
408
487
  def get_block_summary(call_options, fcb)
409
488
  opts = optsmerge call_options
410
489
  # return fcb.body unless opts[:struct]
411
-
412
490
  return fcb unless opts[:bash]
413
491
 
414
492
  fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
@@ -420,16 +498,23 @@ module MarkdownExec
420
498
  bm = option_match_groups(titlexcall, opts[:block_name_match])
421
499
  fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan])
422
500
  fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan])
423
- fcb.title = fcb.name = (bm && bm[1] ? bm[:title] : titlexcall)
501
+
502
+ shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
503
+ fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
504
+ fcb.dname = if shell_color_option && opts[shell_color_option].present?
505
+ fcb.oname.send(opts[shell_color_option].to_sym)
506
+ else
507
+ fcb.oname
508
+ end
424
509
  fcb
425
510
  end
426
511
 
427
512
  # :reek:DuplicateMethodCall
428
513
  # :reek:LongYieldList
429
514
  # :reek:NestedIterators
430
- def iter_blocks_in_file(opts = {})
431
- # opts = optsmerge call_options, options_block
515
+ #---
432
516
 
517
+ def iter_blocks_in_file(opts = {}, &block)
433
518
  unless opts[:filename]&.present?
434
519
  fout 'No blocks found.'
435
520
  return
@@ -440,10 +525,10 @@ module MarkdownExec
440
525
  return
441
526
  end
442
527
 
443
- fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
444
- fenced_start_ex = Regexp.new opts[:fenced_start_ex_match]
528
+ fenced_start_and_end_regex = Regexp.new opts[:fenced_start_and_end_regex]
529
+ fenced_start_extended_regex = Regexp.new opts[:fenced_start_extended_regex]
445
530
  fcb = FCB.new
446
- in_block = false
531
+ in_fenced_block = false
447
532
  headings = []
448
533
 
449
534
  ## get type of messages to select
@@ -452,70 +537,105 @@ module MarkdownExec
452
537
 
453
538
  cfile.readlines(opts[:filename]).each.with_index do |line, _line_num|
454
539
  continue unless line
540
+ headings = update_headings(line, headings, opts) if opts[:menu_blocks_with_headings]
455
541
 
456
- if opts[:menu_blocks_with_headings]
457
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
458
- headings = [headings[0], headings[1], lm[:name]]
459
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
460
- headings = [headings[0], lm[:name]]
461
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
462
- headings = [lm[:name]]
463
- end
464
- end
465
-
466
- if line.match(fenced_start_and_end_match)
467
- if in_block
468
- # end fcb
469
- #
470
- fcb.name = fcb.title || ''
471
- if fcb.body
472
- if fcb.title.nil? || fcb.title.empty?
473
- fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64]
474
- end
475
-
476
- if block_given? && selected_messages.include?(:blocks) &&
477
- Filter.fcb_select?(opts, fcb)
478
- yield :blocks, fcb
479
- end
480
- end
481
- in_block = false
542
+ if line.match(fenced_start_and_end_regex)
543
+ if in_fenced_block
544
+ process_fenced_block(fcb, opts, selected_messages, &block)
545
+ in_fenced_block = false
482
546
  else
483
- # start fcb
484
- #
485
- in_block = true
486
-
487
- fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys
488
- fcb = FCB.new
489
- fcb.headings = headings
490
- fcb.name = fcb_title_groups.fetch(:name, '')
491
- fcb.shell = fcb_title_groups.fetch(:shell, '')
492
- fcb.title = fcb_title_groups.fetch(:name, '')
493
-
494
- # selected fcb
495
- #
496
- fcb.body = []
497
-
498
- rest = fcb_title_groups.fetch(:rest, '')
499
- fcb.reqs = rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }
500
-
501
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
502
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
503
- tn.named_captures.sym_keys
504
- end
505
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
506
- tn.named_captures.sym_keys
507
- end
547
+ fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
548
+ in_fenced_block = true
508
549
  end
509
- elsif in_block && fcb.body
550
+ elsif in_fenced_block && fcb.body
510
551
  dp 'append line to fcb body'
511
552
  fcb.body += [line.chomp]
512
- elsif block_given? && selected_messages.include?(:line)
513
- dp 'text outside of fcb'
514
- fcb = FCB.new
515
- fcb.body = [line]
516
- yield :line, fcb
553
+ else
554
+ process_line(line, opts, selected_messages, &block)
555
+ end
556
+ end
557
+ end
558
+
559
+ def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
560
+ fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
561
+ fcb = FCB.new
562
+ fcb.headings = headings
563
+ fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
564
+ fcb.shell = fcb_title_groups.fetch(:shell, '')
565
+ fcb.title = fcb_title_groups.fetch(:name, '')
566
+
567
+ # selected fcb
568
+ fcb.body = []
569
+
570
+ rest = fcb_title_groups.fetch(:rest, '')
571
+ fcb.reqs, fcb.wraps =
572
+ split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
573
+ !name.match(Regexp.new(opts[:block_name_wrapper_match]))
574
+ end
575
+ fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
576
+ fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
577
+ tn.named_captures.sym_keys
578
+ end
579
+ fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
580
+ tn.named_captures.sym_keys
581
+ end
582
+ fcb
583
+ end
584
+
585
+ def process_fenced_block(fcb, opts, selected_messages, &block)
586
+ fcb.oname = fcb.dname = fcb.title || ''
587
+ return unless fcb.body
588
+
589
+ set_fcb_title(fcb)
590
+
591
+ if block &&
592
+ selected_messages.include?(:blocks) &&
593
+ Filter.fcb_select?(opts, fcb)
594
+ block.call :blocks, fcb
595
+ end
596
+ end
597
+
598
+ def process_line(line, _opts, selected_messages, &block)
599
+ return unless block && selected_messages.include?(:line)
600
+
601
+ # dp 'text outside of fcb'
602
+ fcb = FCB.new
603
+ fcb.body = [line]
604
+ block.call(:line, fcb)
605
+ end
606
+
607
+ # set the title of an FCB object based on its body if it is nil or empty
608
+ def set_fcb_title(fcb)
609
+ return unless fcb.title.nil? || fcb.title.empty?
610
+
611
+ fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
612
+ end
613
+
614
+ def split_array(arr)
615
+ true_list = []
616
+ false_list = []
617
+
618
+ arr.each do |element|
619
+ if yield(element)
620
+ true_list << element
621
+ else
622
+ false_list << element
517
623
  end
518
624
  end
625
+
626
+ [true_list, false_list]
627
+ end
628
+
629
+ def update_headings(line, headings, opts)
630
+ if (lm = line.match(Regexp.new(opts[:heading3_match])))
631
+ [headings[0], headings[1], lm[:name]]
632
+ elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
633
+ [headings[0], lm[:name]]
634
+ elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
635
+ [lm[:name]]
636
+ else
637
+ headings
638
+ end
519
639
  end
520
640
 
521
641
  # return body, title if option.struct
@@ -530,10 +650,11 @@ module MarkdownExec
530
650
  blocks.push FCB.new({
531
651
  # name: '',
532
652
  chrome: true,
533
- name: format(
653
+ dname: format(
534
654
  opts[:menu_divider_format],
535
655
  opts[:menu_initial_divider]
536
656
  ).send(opts[:menu_divider_color].to_sym),
657
+ oname: opts[:menu_initial_divider],
537
658
  disabled: '' # __LINE__.to_s
538
659
  })
539
660
  end
@@ -554,8 +675,9 @@ module MarkdownExec
554
675
  blocks.push FCB.new(
555
676
  { chrome: true,
556
677
  disabled: '',
557
- name: format(opts[:menu_divider_format],
558
- mbody[:name]).send(opts[:menu_divider_color].to_sym) }
678
+ dname: format(opts[:menu_divider_format],
679
+ mbody[:name]).send(opts[:menu_divider_color].to_sym),
680
+ oname: mbody[:name] }
559
681
  )
560
682
  end
561
683
  elsif opts[:menu_task_match].present? &&
@@ -564,10 +686,14 @@ module MarkdownExec
564
686
  blocks.push FCB.new(
565
687
  { chrome: true,
566
688
  disabled: '',
567
- name: format(
689
+ dname: format(
690
+ opts[:menu_task_format],
691
+ $~.named_captures.transform_keys(&:to_sym)
692
+ ).send(opts[:menu_task_color].to_sym),
693
+ oname: format(
568
694
  opts[:menu_task_format],
569
695
  $~.named_captures.transform_keys(&:to_sym)
570
- ).send(opts[:menu_task_color].to_sym) }
696
+ ) }
571
697
  )
572
698
  end
573
699
  else
@@ -584,9 +710,10 @@ module MarkdownExec
584
710
  blocks.push FCB.new(
585
711
  { chrome: true,
586
712
  disabled: '',
587
- name: format(opts[:menu_divider_format],
588
- opts[:menu_final_divider])
589
- .send(opts[:menu_divider_color].to_sym) }
713
+ dname: format(opts[:menu_divider_format],
714
+ opts[:menu_final_divider])
715
+ .send(opts[:menu_divider_color].to_sym),
716
+ oname: opts[:menu_final_divider] }
590
717
  )
591
718
  end
592
719
  blocks
@@ -667,7 +794,7 @@ module MarkdownExec
667
794
  else
668
795
  # blocks.map(&:name)
669
796
  blocks.map do |block|
670
- block.fetch(:text, nil) || block.fetch(:name, nil)
797
+ block.fetch(:text, nil) || block.oname
671
798
  end
672
799
  end.compact.reject(&:empty?)
673
800
  end
@@ -760,10 +887,10 @@ module MarkdownExec
760
887
  when :line
761
888
  if options[:menu_divider_match] &&
762
889
  (mbody = fcb.body[0].match(options[:menu_divider_match]))
763
- menu.push FCB.new({ name: mbody[:name], disabled: '' })
890
+ menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
764
891
  end
765
892
  when :blocks
766
- menu += [fcb.name]
893
+ menu += [fcb.oname]
767
894
  end
768
895
  end
769
896
  menu
@@ -809,7 +936,7 @@ module MarkdownExec
809
936
  def options_finalize(rest)
810
937
  ## position 0: file or folder (optional)
811
938
  #
812
- if (pos = rest.fetch(0, nil))&.present?
939
+ if (pos = rest.shift)&.present?
813
940
  if Dir.exist?(pos)
814
941
  @options[:path] = pos
815
942
  elsif File.exist?(pos)
@@ -821,7 +948,7 @@ module MarkdownExec
821
948
 
822
949
  ## position 1: block name (optional)
823
950
  #
824
- block_name = rest.fetch(1, nil)
951
+ block_name = rest.shift
825
952
  @options[:block_name] = block_name if block_name.present?
826
953
  end
827
954
 
@@ -868,13 +995,31 @@ module MarkdownExec
868
995
  }
869
996
  end
870
997
 
998
+ ## insert back option at head or tail
999
+ #
1000
+ ## Adds a back option at the head or tail of a menu
1001
+ #
1002
+ def prompt_menu_add_back(items, label = BACK_OPTION)
1003
+ return items unless @options[:menu_with_back]
1004
+
1005
+ history = ENV.fetch('MDE_MENU_HISTORY', '')
1006
+ return items unless history.present?
1007
+
1008
+ @hs_curr, @hs_rest = split_string_on_first_char(
1009
+ history,
1010
+ @options[:history_document_separator]
1011
+ )
1012
+
1013
+ @options[:menu_back_at_top] ? [label] + items : items + [label]
1014
+ end
1015
+
871
1016
  ## insert exit option at head or tail
872
1017
  #
873
- def prompt_menu_add_exit(_prompt_text, items, exit_option, _opts = {})
1018
+ def prompt_menu_add_exit(items, label = EXIT_OPTION)
874
1019
  if @options[:menu_exit_at_top]
875
- (@options[:menu_with_exit] ? [exit_option] : []) + items
1020
+ (@options[:menu_with_exit] ? [label] : []) + items
876
1021
  else
877
- items + (@options[:menu_with_exit] ? [exit_option] : [])
1022
+ items + (@options[:menu_with_exit] ? [label] : [])
878
1023
  end
879
1024
  end
880
1025
 
@@ -883,10 +1028,31 @@ module MarkdownExec
883
1028
  # return selected option or nil
884
1029
  #
885
1030
  def prompt_with_quit(prompt_text, items, opts = {})
886
- exit_option = '* Exit'
887
- sel = @prompt.select(prompt_text, prompt_menu_add_exit(prompt_text, items, exit_option, opts),
1031
+ obj = prompt_with_quit2(prompt_text, items, opts)
1032
+ if obj.fetch(:option, nil)
1033
+ nil
1034
+ else
1035
+ obj[:selected]
1036
+ end
1037
+ end
1038
+
1039
+ ## tty prompt to select
1040
+ # insert exit option at head or tail
1041
+ # return option:, selected option:
1042
+ #
1043
+ def prompt_with_quit2(prompt_text, items, opts = {})
1044
+ sel = @prompt.select(prompt_text,
1045
+ prompt_menu_add_exit(
1046
+ prompt_menu_add_back(items)
1047
+ ),
888
1048
  opts.merge(filter: true))
889
- sel == exit_option ? nil : sel
1049
+ if sel == BACK_OPTION
1050
+ { option: sel, curr: @hs_curr, rest: @hs_rest }
1051
+ elsif sel == EXIT_OPTION
1052
+ { option: sel }
1053
+ else
1054
+ { selected: sel }
1055
+ end
890
1056
  end
891
1057
 
892
1058
  # :reek:UtilityFunction ### temp
@@ -899,12 +1065,8 @@ module MarkdownExec
899
1065
 
900
1066
  # :reek:NestedIterators
901
1067
  def run
902
- ## default configuration
903
- #
904
1068
  @options = base_options
905
1069
 
906
- ## read local configuration file
907
- #
908
1070
  read_configuration_file! @options,
909
1071
  ".#{MarkdownExec::APP_NAME.downcase}.yml"
910
1072
 
@@ -920,12 +1082,13 @@ module MarkdownExec
920
1082
  menu_option_append opts, options, item
921
1083
  end
922
1084
  end
923
- option_parser.load # filename defaults to basename of the program
924
- # without suffix in a directory ~/.options
925
- option_parser.environment # env defaults to the basename of the program
926
- # child_argv = arguments_for_child
1085
+ option_parser.load
1086
+ option_parser.environment
927
1087
  rest = option_parser.parse!(arguments_for_mde) # (into: options)
928
1088
 
1089
+ # pass through arguments excluded from OptionParser with `--`
1090
+ @options[:pass_args] = ARGV[rest.count + 1..]
1091
+
929
1092
  begin
930
1093
  options_finalize rest
931
1094
  exec_block options, options[:block_name]
@@ -984,54 +1147,98 @@ module MarkdownExec
984
1147
  File.write(@options[:logged_stdout_filespec], ol.join)
985
1148
  end
986
1149
 
987
- def select_approve_and_execute_block(call_options, &options_block)
988
- opts = optsmerge call_options, options_block
989
- blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
990
- mdoc = MDoc.new(blocks_in_file) do |nopts|
991
- opts.merge!(nopts)
992
- end
993
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1150
+ # Prepare the blocks menu by adding labels and other necessary details.
1151
+ #
1152
+ # @param blocks_in_file [Array<Hash>] The list of blocks from the file.
1153
+ # @param opts [Hash] The options hash.
1154
+ # @return [Array<Hash>] The updated blocks menu.
1155
+ def prepare_blocks_menu(blocks_in_file, opts)
1156
+ # next if fcb.fetch(:disabled, false)
1157
+ # next unless fcb.fetch(:name, '').present?
1158
+ blocks_in_file.map do |fcb|
1159
+ fcb.merge!(
1160
+ name: fcb.dname,
1161
+ label: BlockLabel.make(
1162
+ body: fcb[:body],
1163
+ filename: opts[:filename],
1164
+ headings: fcb.fetch(:headings, []),
1165
+ menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1166
+ menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1167
+ text: fcb[:text],
1168
+ title: fcb[:title]
1169
+ )
1170
+ )
1171
+ fcb.to_h
1172
+ end.compact
1173
+ end
994
1174
 
1175
+ # Select and execute a code block from a Markdown document.
1176
+ #
1177
+ # This method allows the user to interactively select a code block from a
1178
+ # Markdown document, obtain approval, and execute the chosen block of code.
1179
+ #
1180
+ # @param call_options [Hash] Initial options for the method.
1181
+ # @param options_block [Block] Block of options to be merged with call_options.
1182
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
1183
+ def select_approve_and_execute_block(call_options, &options_block)
1184
+ opts = optsmerge(call_options, options_block)
995
1185
  repeat_menu = true && !opts[:block_name].present?
1186
+
1187
+ load_file = !LOAD_FILE
996
1188
  loop do
997
- unless opts[:block_name].present?
998
- pt = (opts[:prompt_select_block]).to_s
999
-
1000
- bm = blocks_menu.map do |fcb|
1001
- # next if fcb.fetch(:disabled, false)
1002
- # next unless fcb.fetch(:name, '').present?
1003
-
1004
- fcb.merge!(
1005
- label: BlockLabel.make(
1006
- body: fcb[:body],
1007
- filename: opts[:filename],
1008
- headings: fcb.fetch(:headings, []),
1009
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1010
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1011
- text: fcb[:text],
1012
- title: fcb[:title]
1013
- )
1014
- )
1189
+ # load file
1190
+ #
1191
+ loop do
1192
+ # repeat menu
1193
+ #
1194
+ load_file = !LOAD_FILE
1195
+ blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
1196
+ mdoc = MDoc.new(blocks_in_file) do |nopts|
1197
+ opts.merge!(nopts)
1198
+ end
1199
+ blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1200
+ unless opts[:block_name].present?
1201
+ pt = opts[:prompt_select_block].to_s
1202
+ bm = prepare_blocks_menu(blocks_menu, opts)
1203
+ return nil if bm.count.zero?
1204
+
1205
+ # sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
1206
+ # return nil if sel.nil?
1207
+ obj = prompt_with_quit2(pt, bm, per_page: opts[:select_page_height])
1208
+ case obj.fetch(:option, nil)
1209
+ when EXIT_OPTION
1210
+ return nil
1211
+ when BACK_OPTION
1212
+ opts[:filename] = obj[:curr]
1213
+ opts[:block_name] = @options[:block_name] = ''
1214
+ ENV['MDE_MENU_HISTORY'] = obj[:rest]
1215
+ load_file = LOAD_FILE # later: exit menu, load file
1216
+ else
1217
+ sel = obj[:selected]
1218
+
1219
+ ## store selected option
1220
+ #
1221
+ label_block = blocks_in_file.select do |fcb|
1222
+ fcb.dname == sel
1223
+ end.fetch(0, nil)
1224
+ opts[:block_name] = @options[:block_name] = label_block.oname
1225
+ end
1226
+ end
1227
+ break if load_file == LOAD_FILE
1015
1228
 
1016
- fcb.to_h
1017
- end.compact
1018
- return nil if bm.count.zero?
1229
+ # later: load file
1019
1230
 
1020
- sel = prompt_with_quit pt, bm,
1021
- per_page: opts[:select_page_height]
1022
- return nil if sel.nil?
1231
+ load_file, block_name = approve_and_execute_block(opts, mdoc)
1023
1232
 
1024
- ## store selected option
1025
- #
1026
- label_block = blocks_in_file.select do |fcb|
1027
- fcb[:label] == sel
1028
- end.fetch(0, nil)
1029
- opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
1030
- end
1031
- approve_and_execute_block opts, mdoc
1032
- break unless repeat_menu
1233
+ opts[:block_name] = block_name
1234
+ if load_file == LOAD_FILE
1235
+ repeat_menu = true
1236
+ break
1237
+ end
1033
1238
 
1034
- opts[:block_name] = ''
1239
+ break unless repeat_menu
1240
+ end
1241
+ break if load_file != LOAD_FILE
1035
1242
  end
1036
1243
  rescue StandardError => err
1037
1244
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1093,6 +1300,22 @@ module MarkdownExec
1093
1300
  end.to_yaml
1094
1301
  end
1095
1302
 
1303
+ # Splits the given string on the first occurrence of the specified character.
1304
+ # Returns an array containing the portion of the string before the character and the rest of the string.
1305
+ #
1306
+ # @param input_str [String] The string to be split.
1307
+ # @param split_char [String] The character on which to split the string.
1308
+ # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
1309
+ def split_string_on_first_char(input_str, split_char)
1310
+ split_index = input_str.index(split_char)
1311
+
1312
+ if split_index.nil?
1313
+ [input_str, '']
1314
+ else
1315
+ [input_str[0...split_index], input_str[(split_index + 1)..-1]]
1316
+ end
1317
+ end
1318
+
1096
1319
  def tab_completions(data = menu_for_optparse)
1097
1320
  data.map do |item|
1098
1321
  "--#{item[:long_name]}" if item[:long_name]
@@ -1110,7 +1333,7 @@ module MarkdownExec
1110
1333
  @options
1111
1334
  end
1112
1335
 
1113
- def write_command_file(call_options, required_blocks)
1336
+ def write_command_file(call_options, required_lines)
1114
1337
  return unless call_options[:save_executed_script]
1115
1338
 
1116
1339
  time_now = Time.now.utc
@@ -1137,7 +1360,7 @@ module MarkdownExec
1137
1360
  "# file_name: #{opts[:filename]}\n" \
1138
1361
  "# block_name: #{opts[:block_name]}\n" \
1139
1362
  "# time: #{time_now}\n" \
1140
- "#{required_blocks.flatten.join("\n")}\n")
1363
+ "#{required_lines.flatten.join("\n")}\n")
1141
1364
  return if @options[:saved_script_chmod].zero?
1142
1365
 
1143
1366
  File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
@@ -1145,4 +1368,69 @@ module MarkdownExec
1145
1368
  end # class MarkParse
1146
1369
  end # module MarkdownExec
1147
1370
 
1148
- require 'minitest/autorun' if $PROGRAM_NAME == __FILE__
1371
+ if $PROGRAM_NAME == __FILE__
1372
+ require 'bundler/setup'
1373
+ Bundler.require(:default)
1374
+
1375
+ require 'minitest/autorun'
1376
+
1377
+ module MarkdownExec
1378
+ class TestMarkParse < Minitest::Test
1379
+ require 'mocha/minitest'
1380
+
1381
+ def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1382
+ pigeon = 'E'
1383
+ obj = { pass_args: pigeon }
1384
+
1385
+ c = MarkdownExec::MarkParse.new
1386
+
1387
+ # Expect that method command_execute is called with argument args having value pigeon
1388
+ c.expects(:command_execute).with(
1389
+ obj,
1390
+ '',
1391
+ args: pigeon)
1392
+
1393
+ # Call method execute_approved_block
1394
+ c.execute_approved_block(obj, [])
1395
+ end
1396
+
1397
+ def setup
1398
+ @mark_parse = MarkdownExec::MarkParse.new
1399
+ end
1400
+
1401
+ def test_set_fcb_title
1402
+ # sample input and output data for testing set_fcb_title method
1403
+ input_output_data = [
1404
+ {
1405
+ input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1406
+ output: "puts 'Hello, world!'"
1407
+ },
1408
+ {
1409
+ input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
1410
+ output: 'def add(x, y) x + y end'
1411
+ },
1412
+ {
1413
+ input: FCB.new(title: 'foo', body: %w[bar baz]),
1414
+ output: 'foo' # expect the title to remain unchanged
1415
+ }
1416
+ ]
1417
+
1418
+ # iterate over the input and output data and assert that the method sets the title as expected
1419
+ input_output_data.each do |data|
1420
+ input = data[:input]
1421
+ output = data[:output]
1422
+ @mark_parse.set_fcb_title(input)
1423
+ assert_equal output, input.title
1424
+ end
1425
+ end
1426
+ end
1427
+
1428
+ ###
1429
+
1430
+ # result = split_string_on_first_char("hello-world", "-")
1431
+ # puts result.inspect # Output should be ["hello", "world"]
1432
+
1433
+ # result = split_string_on_first_char("hello", "-")
1434
+ # puts result.inspect # Output should be ["hello", ""]
1435
+ end
1436
+ end