markdown_exec 1.3.8 → 1.3.9

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.
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