markdown_exec 1.3.8 → 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
  #
@@ -208,124 +223,170 @@ module MarkdownExec
208
223
  # @param opts [Hash] Options hash containing configuration settings.
209
224
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
210
225
  # @return [String] The name of the executed code block.
226
+ #
211
227
  def approve_and_execute_block(opts, mdoc)
212
- # Collect required code blocks based on the provided options.
213
- required_blocks = mdoc.collect_recursively_required_code(opts[:block_name])
214
- # Display required code blocks if requested or required approval.
215
- if opts[:output_script] || opts[:user_must_approve]
216
- display_required_code(opts,
217
- 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, '']
218
247
  end
248
+ end
219
249
 
220
- allow = true
221
- # If user approval is required, prompt the user for approval.
222
- if opts[:user_must_approve]
223
- loop do
224
- # Present a selection menu for user approval.
225
- sel = @prompt.select(opts[:prompt_approve_block],
226
- filter: true) do |menu|
227
- menu.default 1
228
- menu.choice opts[:prompt_yes], 1
229
- menu.choice opts[:prompt_no], 2
230
- menu.choice opts[:prompt_script_to_clipboard], 3
231
- menu.choice opts[:prompt_save_script], 4
232
- end
233
- allow = (sel == 1)
234
- if sel == 3
235
- # Copy the code to the clipboard.
236
- text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
237
- Clipboard.copy(text)
238
- fout "Clipboard updated: #{required_blocks.count} blocks," /
239
- " #{required_blocks.flatten.count} lines," /
240
- " #{text.length} characters"
241
- end
242
- if sel == 4
243
- # Save the code to a file.
244
- write_command_file(opts.merge(save_executed_script: true),
245
- required_blocks)
246
- fout "File saved: #{@options[:saved_filespec]}"
247
- end
248
- break if [1, 2].include? sel
249
- 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
250
262
  end
251
263
 
252
- opts[:ir_approve] = allow
264
+ [LOAD_FILE, data.fetch('block', '')]
265
+ end
253
266
 
254
- # Get the selected code block by name.
255
- selected = mdoc.get_block_by_name(opts[:block_name])
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
256
281
 
257
- # If approved, write the code to a file, execute it, and provide output.
258
- if opts[:ir_approve]
259
- write_command_file(opts, required_blocks)
260
- command_execute(opts, required_blocks.flatten.join("\n"))
261
- save_execution_output
262
- output_execution_summary
263
- output_execution_result
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
264
290
  end
265
291
 
266
- selected[:name]
292
+ if sel == 3
293
+ copy_to_clipboard(required_lines)
294
+ elsif sel == 4
295
+ save_to_file(opts, required_lines)
296
+ end
297
+
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
267
339
  end
268
340
 
269
341
  def cfile
270
342
  @cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern))
271
343
  end
272
344
 
273
- # :reek:DuplicateMethodCall
274
- # :reek:UncommunicativeVariableName { exclude: [ e ] }
275
- # :reek:LongYieldList
276
- def command_execute(opts, command)
277
- #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: [])
278
367
  @execute_files = Hash.new([])
279
368
  @execute_options = opts
280
369
  @execute_started_at = Time.now.utc
281
370
 
282
- args = []
283
- Open3.popen3(@options[:shell], '-c',
284
- command, ARGV[0], *args) do |stdin, stdout, stderr, exec_thr|
285
- #d 'command started'
286
- Thread.new do
287
- until (line = stdout.gets).nil?
288
- @execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
289
- print line if opts[:output_stdout]
290
- yield nil, line, nil, exec_thr if block_given?
291
- end
292
- rescue IOError
293
- #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?
294
375
  end
295
-
296
- Thread.new do
297
- until (line = stderr.gets).nil?
298
- @execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
299
- print line if opts[:output_stdout]
300
- yield nil, nil, line, exec_thr if block_given?
301
- end
302
- rescue IOError
303
- #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?
304
378
  end
305
379
 
306
- in_thr = Thread.new do
307
- while exec_thr.alive? # reading input until the child process ends
308
- stdin.puts(line = $stdin.gets)
309
- @execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
310
- yield line, nil, nil, exec_thr if block_given?
311
- end
312
- #d 'exec_thr now dead'
313
- rescue StandardError
314
- #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?
315
383
  end
316
384
 
317
- #d 'join exec_thr'
318
385
  exec_thr.join
319
-
320
- #d 'wait before closing stdin'
321
386
  sleep 0.1
322
-
323
- #d 'kill stdin thread'
324
- in_thr.kill
325
- # @return_code = exec_thr.value
326
- #d 'command end'
387
+ in_thr.kill if in_thr&.alive?
327
388
  end
328
- #d 'command completed'
389
+
329
390
  @execute_completed_at = Time.now.utc
330
391
  rescue Errno::ENOENT => err
331
392
  #d 'command error ENOENT triggered by missing command in script'
@@ -344,19 +405,19 @@ module MarkdownExec
344
405
  end
345
406
 
346
407
  def count_blocks_in_filename
347
- 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]
348
409
  cnt = 0
349
410
  cfile.readlines(@options[:filename]).each do |line|
350
- cnt += 1 if line.match(fenced_start_and_end_match)
411
+ cnt += 1 if line.match(fenced_start_and_end_regex)
351
412
  end
352
413
  cnt / 2
353
414
  end
354
415
 
355
416
  # :reek:DuplicateMethodCall
356
- def display_required_code(opts, required_blocks)
417
+ def display_required_code(opts, required_lines)
357
418
  frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
358
419
  fout frame
359
- required_blocks.each { |cb| fout cb }
420
+ required_lines.each { |cb| fout cb }
360
421
  fout frame
361
422
  end
362
423
 
@@ -426,7 +487,6 @@ module MarkdownExec
426
487
  def get_block_summary(call_options, fcb)
427
488
  opts = optsmerge call_options
428
489
  # return fcb.body unless opts[:struct]
429
-
430
490
  return fcb unless opts[:bash]
431
491
 
432
492
  fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
@@ -438,16 +498,23 @@ module MarkdownExec
438
498
  bm = option_match_groups(titlexcall, opts[:block_name_match])
439
499
  fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan])
440
500
  fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan])
441
- 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
442
509
  fcb
443
510
  end
444
511
 
445
512
  # :reek:DuplicateMethodCall
446
513
  # :reek:LongYieldList
447
514
  # :reek:NestedIterators
448
- def iter_blocks_in_file(opts = {})
449
- # opts = optsmerge call_options, options_block
515
+ #---
450
516
 
517
+ def iter_blocks_in_file(opts = {}, &block)
451
518
  unless opts[:filename]&.present?
452
519
  fout 'No blocks found.'
453
520
  return
@@ -458,10 +525,10 @@ module MarkdownExec
458
525
  return
459
526
  end
460
527
 
461
- fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
462
- 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]
463
530
  fcb = FCB.new
464
- in_block = false
531
+ in_fenced_block = false
465
532
  headings = []
466
533
 
467
534
  ## get type of messages to select
@@ -470,75 +537,80 @@ module MarkdownExec
470
537
 
471
538
  cfile.readlines(opts[:filename]).each.with_index do |line, _line_num|
472
539
  continue unless line
540
+ headings = update_headings(line, headings, opts) if opts[:menu_blocks_with_headings]
473
541
 
474
- if opts[:menu_blocks_with_headings]
475
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
476
- headings = [headings[0], headings[1], lm[:name]]
477
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
478
- headings = [headings[0], lm[:name]]
479
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
480
- headings = [lm[:name]]
481
- end
482
- end
483
-
484
- if line.match(fenced_start_and_end_match)
485
- if in_block
486
- # end fcb
487
- #
488
- fcb.name = fcb.title || ''
489
- if fcb.body
490
- if fcb.title.nil? || fcb.title.empty?
491
- fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64]
492
- end
493
-
494
- if block_given? &&
495
- selected_messages.include?(:blocks) &&
496
- Filter.fcb_select?(opts, fcb)
497
- yield :blocks, fcb
498
- end
499
- end
500
- 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
501
546
  else
502
- # start fcb
503
- #
504
- in_block = true
505
-
506
- fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys
507
- fcb = FCB.new
508
- fcb.headings = headings
509
- fcb.name = fcb_title_groups.fetch(:name, '')
510
- fcb.shell = fcb_title_groups.fetch(:shell, '')
511
- fcb.title = fcb_title_groups.fetch(:name, '')
512
-
513
- # selected fcb
514
- #
515
- fcb.body = []
516
-
517
- rest = fcb_title_groups.fetch(:rest, '')
518
- fcb.reqs, fcb.wraps =
519
- split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
520
- !name.match(Regexp.new(opts[:block_name_wrapper_match]))
521
- end
522
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
523
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
524
- tn.named_captures.sym_keys
525
- end
526
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
527
- tn.named_captures.sym_keys
528
- end
547
+ fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
548
+ in_fenced_block = true
529
549
  end
530
- elsif in_block && fcb.body
550
+ elsif in_fenced_block && fcb.body
531
551
  dp 'append line to fcb body'
532
552
  fcb.body += [line.chomp]
533
- elsif block_given? && selected_messages.include?(:line)
534
- dp 'text outside of fcb'
535
- fcb = FCB.new
536
- fcb.body = [line]
537
- yield :line, fcb
553
+ else
554
+ process_line(line, opts, selected_messages, &block)
538
555
  end
539
556
  end
540
557
  end
541
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
+
542
614
  def split_array(arr)
543
615
  true_list = []
544
616
  false_list = []
@@ -554,11 +626,17 @@ module MarkdownExec
554
626
  [true_list, false_list]
555
627
  end
556
628
 
557
- # # Example usage:
558
- # array = [1, 2, 3, 4, 5]
559
- # result = split_array(array) { |num| num.even? }
560
- # puts "True List: #{result[0]}" # Output: True List: [2, 4]
561
- # puts "False List: #{result[1]}" # Output: False List: [1, 3, 5]
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
639
+ end
562
640
 
563
641
  # return body, title if option.struct
564
642
  # return body if not struct
@@ -572,10 +650,11 @@ module MarkdownExec
572
650
  blocks.push FCB.new({
573
651
  # name: '',
574
652
  chrome: true,
575
- name: format(
653
+ dname: format(
576
654
  opts[:menu_divider_format],
577
655
  opts[:menu_initial_divider]
578
656
  ).send(opts[:menu_divider_color].to_sym),
657
+ oname: opts[:menu_initial_divider],
579
658
  disabled: '' # __LINE__.to_s
580
659
  })
581
660
  end
@@ -596,8 +675,9 @@ module MarkdownExec
596
675
  blocks.push FCB.new(
597
676
  { chrome: true,
598
677
  disabled: '',
599
- name: format(opts[:menu_divider_format],
600
- 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] }
601
681
  )
602
682
  end
603
683
  elsif opts[:menu_task_match].present? &&
@@ -606,10 +686,14 @@ module MarkdownExec
606
686
  blocks.push FCB.new(
607
687
  { chrome: true,
608
688
  disabled: '',
609
- name: format(
689
+ dname: format(
610
690
  opts[:menu_task_format],
611
691
  $~.named_captures.transform_keys(&:to_sym)
612
- ).send(opts[:menu_task_color].to_sym) }
692
+ ).send(opts[:menu_task_color].to_sym),
693
+ oname: format(
694
+ opts[:menu_task_format],
695
+ $~.named_captures.transform_keys(&:to_sym)
696
+ ) }
613
697
  )
614
698
  end
615
699
  else
@@ -626,9 +710,10 @@ module MarkdownExec
626
710
  blocks.push FCB.new(
627
711
  { chrome: true,
628
712
  disabled: '',
629
- name: format(opts[:menu_divider_format],
630
- opts[:menu_final_divider])
631
- .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] }
632
717
  )
633
718
  end
634
719
  blocks
@@ -709,7 +794,7 @@ module MarkdownExec
709
794
  else
710
795
  # blocks.map(&:name)
711
796
  blocks.map do |block|
712
- block.fetch(:text, nil) || block.fetch(:name, nil)
797
+ block.fetch(:text, nil) || block.oname
713
798
  end
714
799
  end.compact.reject(&:empty?)
715
800
  end
@@ -802,10 +887,10 @@ module MarkdownExec
802
887
  when :line
803
888
  if options[:menu_divider_match] &&
804
889
  (mbody = fcb.body[0].match(options[:menu_divider_match]))
805
- menu.push FCB.new({ name: mbody[:name], disabled: '' })
890
+ menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
806
891
  end
807
892
  when :blocks
808
- menu += [fcb.name]
893
+ menu += [fcb.oname]
809
894
  end
810
895
  end
811
896
  menu
@@ -851,7 +936,7 @@ module MarkdownExec
851
936
  def options_finalize(rest)
852
937
  ## position 0: file or folder (optional)
853
938
  #
854
- if (pos = rest.fetch(0, nil))&.present?
939
+ if (pos = rest.shift)&.present?
855
940
  if Dir.exist?(pos)
856
941
  @options[:path] = pos
857
942
  elsif File.exist?(pos)
@@ -863,7 +948,7 @@ module MarkdownExec
863
948
 
864
949
  ## position 1: block name (optional)
865
950
  #
866
- block_name = rest.fetch(1, nil)
951
+ block_name = rest.shift
867
952
  @options[:block_name] = block_name if block_name.present?
868
953
  end
869
954
 
@@ -910,13 +995,31 @@ module MarkdownExec
910
995
  }
911
996
  end
912
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
+
913
1016
  ## insert exit option at head or tail
914
1017
  #
915
- def prompt_menu_add_exit(_prompt_text, items, exit_option, _opts = {})
1018
+ def prompt_menu_add_exit(items, label = EXIT_OPTION)
916
1019
  if @options[:menu_exit_at_top]
917
- (@options[:menu_with_exit] ? [exit_option] : []) + items
1020
+ (@options[:menu_with_exit] ? [label] : []) + items
918
1021
  else
919
- items + (@options[:menu_with_exit] ? [exit_option] : [])
1022
+ items + (@options[:menu_with_exit] ? [label] : [])
920
1023
  end
921
1024
  end
922
1025
 
@@ -925,10 +1028,31 @@ module MarkdownExec
925
1028
  # return selected option or nil
926
1029
  #
927
1030
  def prompt_with_quit(prompt_text, items, opts = {})
928
- exit_option = '* Exit'
929
- 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
+ ),
930
1048
  opts.merge(filter: true))
931
- 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
932
1056
  end
933
1057
 
934
1058
  # :reek:UtilityFunction ### temp
@@ -941,12 +1065,8 @@ module MarkdownExec
941
1065
 
942
1066
  # :reek:NestedIterators
943
1067
  def run
944
- ## default configuration
945
- #
946
1068
  @options = base_options
947
1069
 
948
- ## read local configuration file
949
- #
950
1070
  read_configuration_file! @options,
951
1071
  ".#{MarkdownExec::APP_NAME.downcase}.yml"
952
1072
 
@@ -962,12 +1082,13 @@ module MarkdownExec
962
1082
  menu_option_append opts, options, item
963
1083
  end
964
1084
  end
965
- option_parser.load # filename defaults to basename of the program
966
- # without suffix in a directory ~/.options
967
- option_parser.environment # env defaults to the basename of the program
968
- # child_argv = arguments_for_child
1085
+ option_parser.load
1086
+ option_parser.environment
969
1087
  rest = option_parser.parse!(arguments_for_mde) # (into: options)
970
1088
 
1089
+ # pass through arguments excluded from OptionParser with `--`
1090
+ @options[:pass_args] = ARGV[rest.count + 1..]
1091
+
971
1092
  begin
972
1093
  options_finalize rest
973
1094
  exec_block options, options[:block_name]
@@ -1026,6 +1147,31 @@ module MarkdownExec
1026
1147
  File.write(@options[:logged_stdout_filespec], ol.join)
1027
1148
  end
1028
1149
 
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
1174
+
1029
1175
  # Select and execute a code block from a Markdown document.
1030
1176
  #
1031
1177
  # This method allows the user to interactively select a code block from a
@@ -1036,51 +1182,63 @@ module MarkdownExec
1036
1182
  # @return [Nil] Returns nil if no code block is selected or an error occurs.
1037
1183
  def select_approve_and_execute_block(call_options, &options_block)
1038
1184
  opts = optsmerge(call_options, options_block)
1039
- blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
1040
- mdoc = MDoc.new(blocks_in_file) do |nopts|
1041
- opts.merge!(nopts)
1042
- end
1043
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1044
-
1045
1185
  repeat_menu = true && !opts[:block_name].present?
1186
+
1187
+ load_file = !LOAD_FILE
1046
1188
  loop do
1047
- unless opts[:block_name].present?
1048
- pt = opts[:prompt_select_block].to_s
1049
-
1050
- bm = blocks_menu.map do |fcb|
1051
- # next if fcb.fetch(:disabled, false)
1052
- # next unless fcb.fetch(:name, '').present?
1053
-
1054
- fcb.merge!(
1055
- label: BlockLabel.make(
1056
- body: fcb[:body],
1057
- filename: opts[:filename],
1058
- headings: fcb.fetch(:headings, []),
1059
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1060
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1061
- text: fcb[:text],
1062
- title: fcb[:title]
1063
- )
1064
- )
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
1065
1228
 
1066
- fcb.to_h
1067
- end.compact
1068
- return nil if bm.count.zero?
1229
+ # later: load file
1069
1230
 
1070
- sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
1071
- return nil if sel.nil?
1231
+ load_file, block_name = approve_and_execute_block(opts, mdoc)
1072
1232
 
1073
- ## store selected option
1074
- #
1075
- label_block = blocks_in_file.select do |fcb|
1076
- fcb[:label] == sel
1077
- end.fetch(0, nil)
1078
- opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
1079
- end
1080
- approve_and_execute_block(opts, mdoc)
1081
- break unless repeat_menu
1233
+ opts[:block_name] = block_name
1234
+ if load_file == LOAD_FILE
1235
+ repeat_menu = true
1236
+ break
1237
+ end
1082
1238
 
1083
- opts[:block_name] = ''
1239
+ break unless repeat_menu
1240
+ end
1241
+ break if load_file != LOAD_FILE
1084
1242
  end
1085
1243
  rescue StandardError => err
1086
1244
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1142,6 +1300,22 @@ module MarkdownExec
1142
1300
  end.to_yaml
1143
1301
  end
1144
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
+
1145
1319
  def tab_completions(data = menu_for_optparse)
1146
1320
  data.map do |item|
1147
1321
  "--#{item[:long_name]}" if item[:long_name]
@@ -1159,7 +1333,7 @@ module MarkdownExec
1159
1333
  @options
1160
1334
  end
1161
1335
 
1162
- def write_command_file(call_options, required_blocks)
1336
+ def write_command_file(call_options, required_lines)
1163
1337
  return unless call_options[:save_executed_script]
1164
1338
 
1165
1339
  time_now = Time.now.utc
@@ -1186,7 +1360,7 @@ module MarkdownExec
1186
1360
  "# file_name: #{opts[:filename]}\n" \
1187
1361
  "# block_name: #{opts[:block_name]}\n" \
1188
1362
  "# time: #{time_now}\n" \
1189
- "#{required_blocks.flatten.join("\n")}\n")
1363
+ "#{required_lines.flatten.join("\n")}\n")
1190
1364
  return if @options[:saved_script_chmod].zero?
1191
1365
 
1192
1366
  File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
@@ -1194,4 +1368,69 @@ module MarkdownExec
1194
1368
  end # class MarkParse
1195
1369
  end # module MarkdownExec
1196
1370
 
1197
- 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