markdown_exec 1.4.1 → 1.5

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
@@ -38,10 +38,6 @@ $stdout.sync = true
38
38
 
39
39
  MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
40
40
 
41
- # macros
42
- #
43
- LOAD_FILE = true
44
-
45
41
  # custom error: file specified is missing
46
42
  #
47
43
  class FileMissingError < StandardError; end
@@ -63,6 +59,17 @@ class Hash
63
59
  end
64
60
  end
65
61
 
62
+ class LoadFile
63
+ Load = true
64
+ Reuse = false
65
+ end
66
+
67
+ class MenuState
68
+ BACK = :back
69
+ CONTINUE = :continue
70
+ EXIT = :exit
71
+ end
72
+
66
73
  # integer value for comparison
67
74
  #
68
75
  def options_fetch_display_level(options)
@@ -110,16 +117,23 @@ def dp(str)
110
117
  lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
111
118
  end
112
119
 
120
+ def rpry
121
+ require 'pry-nav'
122
+ require 'pry-stack_explorer'
123
+ end
124
+
113
125
  public
114
126
 
115
127
  # :reek:UtilityFunction
116
- def list_recent_output(saved_stdout_folder, saved_stdout_glob, list_count)
128
+ def list_recent_output(saved_stdout_folder, saved_stdout_glob,
129
+ list_count)
117
130
  SavedFilesMatcher.most_recent_list(saved_stdout_folder,
118
131
  saved_stdout_glob, list_count)
119
132
  end
120
133
 
121
134
  # :reek:UtilityFunction
122
- def list_recent_scripts(saved_script_folder, saved_script_glob, list_count)
135
+ def list_recent_scripts(saved_script_folder, saved_script_glob,
136
+ list_count)
123
137
  SavedFilesMatcher.most_recent_list(saved_script_folder,
124
138
  saved_script_glob, list_count)
125
139
  end
@@ -174,10 +188,10 @@ module MarkdownExec
174
188
  FNR12 = ',~'
175
189
 
176
190
  SHELL_COLOR_OPTIONS = {
177
- BLOCK_TYPE_BASH => :menu_bash_color,
178
- BLOCK_TYPE_LINK => :menu_link_color,
179
- BLOCK_TYPE_OPTS => :menu_opts_color,
180
- BLOCK_TYPE_VARS => :menu_vars_color
191
+ BlockType::BASH => :menu_bash_color,
192
+ BlockType::LINK => :menu_link_color,
193
+ BlockType::OPTS => :menu_opts_color,
194
+ BlockType::VARS => :menu_vars_color
181
195
  }.freeze
182
196
 
183
197
  ##
@@ -209,6 +223,22 @@ module MarkdownExec
209
223
  @prompt = tty_prompt_without_disabled_symbol
210
224
  end
211
225
 
226
+ # Adds Back and Exit options to the CLI menu
227
+ #
228
+ # @param blocks_in_file [Array] The current blocks in the menu
229
+ def add_menu_chrome_blocks!(blocks_in_file)
230
+ return unless @options[:menu_link_format].present?
231
+
232
+ if @options[:menu_with_back] && history_state_exist?
233
+ append_chrome_block(blocks_in_file, MenuState::BACK)
234
+ end
235
+ if @options[:menu_with_exit]
236
+ append_chrome_block(blocks_in_file, MenuState::EXIT)
237
+ end
238
+ append_divider(blocks_in_file, @options, :initial)
239
+ append_divider(blocks_in_file, @options, :final)
240
+ end
241
+
212
242
  ##
213
243
  # Appends a summary of a block (FCB) to the blocks array.
214
244
  #
@@ -218,38 +248,58 @@ module MarkdownExec
218
248
  blocks.push get_block_summary(opts, fcb)
219
249
  end
220
250
 
221
- ##
222
- # Appends a final divider to the blocks array if it is specified in options.
251
+ # Appends a chrome block, which is a menu option for Back or Exit
223
252
  #
224
- def append_final_divider(blocks, opts)
225
- return unless opts[:menu_divider_format].present? && opts[:menu_final_divider].present?
253
+ # @param blocks_in_file [Array] The current blocks in the menu
254
+ # @param type [Symbol] The type of chrome block to add (:back or :exit)
255
+ def append_chrome_block(blocks_in_file, type)
256
+ case type
257
+ when MenuState::BACK
258
+ state = history_state_partition(@options)
259
+ @hs_curr = state[:unit]
260
+ @hs_rest = state[:rest]
261
+ option_name = @options[:menu_option_back_name]
262
+ insert_at_top = @options[:menu_back_at_top]
263
+ when MenuState::EXIT
264
+ option_name = @options[:menu_option_exit_name]
265
+ insert_at_top = @options[:menu_exit_at_top]
266
+ end
226
267
 
227
- blocks.push FCB.new(
228
- { chrome: true,
229
- disabled: '',
230
- dname: format(opts[:menu_divider_format],
231
- opts[:menu_final_divider])
232
- .send(opts[:menu_divider_color].to_sym),
233
- oname: opts[:menu_final_divider] }
268
+ formatted_name = format(@options[:menu_link_format],
269
+ safeval(option_name))
270
+ chrome_block = FCB.new(
271
+ chrome: true,
272
+ dname: formatted_name.send(@options[:menu_link_color].to_sym),
273
+ oname: formatted_name
234
274
  )
275
+
276
+ if insert_at_top
277
+ blocks_in_file.unshift(chrome_block)
278
+ else
279
+ blocks_in_file.push(chrome_block)
280
+ end
235
281
  end
236
282
 
237
- ##
238
- # Appends an initial divider to the blocks array if it is specified in options.
239
- #
240
- def append_initial_divider(blocks, opts)
241
- return unless opts[:menu_initial_divider].present?
283
+ # Appends a divider to the blocks array.
284
+ # @param blocks [Array] The array of block elements.
285
+ # @param opts [Hash] Options containing divider configuration.
286
+ # @param position [Symbol] :initial or :final divider position.
287
+ def append_divider(blocks, opts, position)
288
+ divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider
289
+ unless opts[:menu_divider_format].present? && opts[divider_key].present?
290
+ return
291
+ end
292
+
293
+ oname = format(opts[:menu_divider_format],
294
+ safeval(opts[divider_key]))
295
+ divider = FCB.new(
296
+ chrome: true,
297
+ disabled: '',
298
+ dname: oname.send(opts[:menu_divider_color].to_sym),
299
+ oname: oname
300
+ )
242
301
 
243
- blocks.push FCB.new({
244
- # name: '',
245
- chrome: true,
246
- dname: format(
247
- opts[:menu_divider_format],
248
- opts[:menu_initial_divider]
249
- ).send(opts[:menu_divider_color].to_sym),
250
- oname: opts[:menu_initial_divider],
251
- disabled: '' # __LINE__.to_s
252
- })
302
+ position == :initial ? blocks.unshift(divider) : blocks.push(divider)
253
303
  end
254
304
 
255
305
  # Execute a code block after approval and provide user interaction options.
@@ -261,14 +311,12 @@ module MarkdownExec
261
311
  # @param opts [Hash] Options hash containing configuration settings.
262
312
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
263
313
  #
264
- def approve_and_execute_block(opts, mdoc)
265
- selected = mdoc.get_block_by_name(opts[:block_name])
266
-
267
- if selected.fetch(:shell, '') == BLOCK_TYPE_LINK
314
+ def approve_and_execute_block(selected, opts, mdoc)
315
+ if selected.fetch(:shell, '') == BlockType::LINK
268
316
  handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
269
- elsif opts.fetch(:back, false)
317
+ elsif opts.fetch(:s_back, false)
270
318
  handle_back_link(opts)
271
- elsif selected[:shell] == BLOCK_TYPE_OPTS
319
+ elsif selected[:shell] == BlockType::OPTS
272
320
  handle_shell_opts(opts, selected)
273
321
  else
274
322
  handle_remainder_blocks(mdoc, opts, selected)
@@ -302,10 +350,23 @@ module MarkdownExec
302
350
  env_str(item[:env_var],
303
351
  default: OptionValue.for_hash(item_default))
304
352
  end
305
- [item[:opt_name], item[:proccode] ? item[:proccode].call(value) : value]
353
+ [item[:opt_name],
354
+ item[:proccode] ? item[:proccode].call(value) : value]
306
355
  end.compact.to_h
307
356
  end
308
357
 
358
+ # Finds the first hash-like element within an enumerable collection where the specified key
359
+ # matches the given value. Returns a default value if no match is found.
360
+ #
361
+ # @param blocks [Enumerable] An enumerable collection of hash-like objects.
362
+ # @param key [Object] The key to look up in each hash-like object.
363
+ # @param value [Object] The value to compare against the value associated with the key.
364
+ # @param default [Object] The default value to return if no match is found (optional).
365
+ # @return [Object, nil] The found hash-like object, or the default value if no match is found.
366
+ def block_find(blocks, key, value, default = nil)
367
+ blocks.find { |item| item[key] == value } || default
368
+ end
369
+
309
370
  def blocks_per_opts(blocks, opts)
310
371
  return blocks if opts[:struct]
311
372
 
@@ -347,7 +408,7 @@ module MarkdownExec
347
408
  # @return [Array<String>] Required code blocks as an array of lines.
348
409
  def collect_required_code_lines(mdoc, selected, opts: {})
349
410
  # Apply hash in opts block to environment variables
350
- if selected[:shell] == BLOCK_TYPE_VARS
411
+ if selected[:shell] == BlockType::VARS
351
412
  data = YAML.load(selected[:body].join("\n"))
352
413
  data.each_key do |key|
353
414
  ENV[key] = value = data[key].to_s
@@ -361,7 +422,8 @@ module MarkdownExec
361
422
  end
362
423
  end
363
424
 
364
- required = mdoc.collect_recursively_required_code(opts[:block_name], opts: opts)
425
+ required = mdoc.collect_recursively_required_code(opts[:block_name],
426
+ opts: opts)
365
427
  read_required_blocks_from_temp_file + required[:code]
366
428
  end
367
429
 
@@ -417,6 +479,20 @@ module MarkdownExec
417
479
  fout "Error ENOENT: #{err.inspect}"
418
480
  end
419
481
 
482
+ def command_or_user_selected_block(blocks_in_file, blocks_menu, default,
483
+ opts)
484
+ if opts[:block_name].present?
485
+ block = blocks_in_file.find do |item|
486
+ item[:oname] == opts[:block_name]
487
+ end
488
+ else
489
+ block, state = wait_for_user_selected_block(blocks_in_file, blocks_menu, default,
490
+ opts)
491
+ end
492
+
493
+ [block, state]
494
+ end
495
+
420
496
  def copy_to_clipboard(required_lines)
421
497
  text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
422
498
  Clipboard.copy(text)
@@ -434,7 +510,48 @@ module MarkdownExec
434
510
  cnt / 2
435
511
  end
436
512
 
437
- def create_and_write_file_with_permissions(file_path, content, chmod_value)
513
+ ##
514
+ # Creates and adds a formatted block to the blocks array based on the provided match and format options.
515
+ # @param blocks [Array] The array of blocks to add the new block to.
516
+ # @param fcb [FCB] The file control block containing the line to match against.
517
+ # @param match_data [MatchData] The match data containing named captures for formatting.
518
+ # @param format_option [String] The format string to be used for the new block.
519
+ # @param color_method [Symbol] The color method to apply to the block's display name.
520
+ def create_and_add_chrome_block(blocks, _fcb, match_data, format_option,
521
+ color_method)
522
+ oname = format(format_option,
523
+ match_data.named_captures.transform_keys(&:to_sym))
524
+ blocks.push FCB.new(
525
+ chrome: true,
526
+ disabled: '',
527
+ dname: oname.send(color_method),
528
+ oname: oname
529
+ )
530
+ end
531
+
532
+ ##
533
+ # Processes lines within the file and converts them into blocks if they match certain criteria.
534
+ # @param blocks [Array] The array to append new blocks to.
535
+ # @param fcb [FCB] The file control block being processed.
536
+ # @param opts [Hash] Options containing configuration for line processing.
537
+ # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
538
+ def create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
539
+ return unless use_chrome
540
+
541
+ if opts[:menu_note_match].present? && (mbody = fcb.body[0].match opts[:menu_note_match])
542
+ create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_note_format],
543
+ opts[:menu_note_color].to_sym)
544
+ elsif opts[:menu_divider_match].present? && (mbody = fcb.body[0].match opts[:menu_divider_match])
545
+ create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_divider_format],
546
+ opts[:menu_divider_color].to_sym)
547
+ elsif opts[:menu_task_match].present? && (mbody = fcb.body[0].match opts[:menu_task_match])
548
+ create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_task_format],
549
+ opts[:menu_task_color].to_sym)
550
+ end
551
+ end
552
+
553
+ def create_and_write_file_with_permissions(file_path, content,
554
+ chmod_value)
438
555
  dirname = File.dirname(file_path)
439
556
  FileUtils.mkdir_p dirname
440
557
  File.write(file_path, content)
@@ -450,13 +567,29 @@ module MarkdownExec
450
567
  def delete_required_temp_file
451
568
  temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
452
569
 
453
- return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
570
+ if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
571
+ return
572
+ end
454
573
 
455
574
  FileUtils.rm_f(temp_blocks_file_path)
456
575
 
457
576
  clear_required_file
458
577
  end
459
578
 
579
+ # Derives a title from the body of an FCB object.
580
+ # @param fcb [Object] The FCB object whose title is to be derived.
581
+ # @return [String] The derived title.
582
+ def derive_title_from_body(fcb)
583
+ body_content = fcb&.body
584
+ return '' unless body_content
585
+
586
+ if body_content.count == 1
587
+ body_content.first
588
+ else
589
+ format_multiline_body_as_title(body_content)
590
+ end
591
+ end
592
+
460
593
  ## Determines the correct filename to use for searching files
461
594
  #
462
595
  def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
@@ -464,7 +597,8 @@ module MarkdownExec
464
597
  if specified_filename&.present?
465
598
  return specified_filename if specified_filename.start_with?('/')
466
599
 
467
- File.join(specified_folder || default_folder, specified_filename)
600
+ File.join(specified_folder || default_folder,
601
+ specified_filename)
468
602
  elsif specified_folder&.present?
469
603
  File.join(specified_folder,
470
604
  filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
@@ -486,7 +620,7 @@ module MarkdownExec
486
620
  command_execute(
487
621
  opts,
488
622
  required_lines.flatten.join("\n"),
489
- args: opts.fetch(:pass_args, [])
623
+ args: opts.fetch(:s_pass_args, [])
490
624
  )
491
625
  initialize_and_save_execution_output
492
626
  output_execution_summary
@@ -496,20 +630,22 @@ module MarkdownExec
496
630
  # Reports and executes block logic
497
631
  def execute_block_logic(files)
498
632
  @options[:filename] = select_document_if_multiple(files)
499
- select_approve_and_execute_block({
500
- bash: true,
501
- struct: true
502
- })
633
+ select_approve_and_execute_block({ bash: true,
634
+ struct: true })
503
635
  end
504
636
 
505
637
  ## Executes the block specified in the options
506
638
  #
507
639
  def execute_block_with_error_handling(rest)
508
640
  finalize_cli_argument_processing(rest)
509
- @options[:cli_rest] = rest
641
+ @options[:s_cli_rest] = rest
510
642
  execute_code_block_based_on_options(@options)
511
643
  rescue FileMissingError => err
512
644
  puts "File missing: #{err}"
645
+ rescue StandardError => err
646
+ warn(error = "ERROR ** MarkParse.execute_block_with_error_handling(); #{err.inspect}")
647
+ binding.pry if $tap_enable
648
+ raise ArgumentError, error
513
649
  end
514
650
 
515
651
  # Main method to execute a block based on options and block_name
@@ -521,7 +657,8 @@ module MarkdownExec
521
657
  doc_glob: -> { fout options[:md_filename_glob] },
522
658
  list_blocks: lambda do
523
659
  fout_list (files.map do |file|
524
- make_block_labels(filename: file, struct: true)
660
+ menu_with_block_labels(filename: file,
661
+ struct: true)
525
662
  end).flatten(1)
526
663
  end,
527
664
  list_default_yaml: -> { fout_list list_default_yaml },
@@ -571,15 +708,6 @@ module MarkdownExec
571
708
  false
572
709
  end
573
710
 
574
- ##
575
- # Determines the types of blocks to select based on the filter.
576
- #
577
- def filter_block_types
578
- ## return type of blocks to select
579
- #
580
- %i[blocks line]
581
- end
582
-
583
711
  ## post-parse options configuration
584
712
  #
585
713
  def finalize_cli_argument_processing(rest)
@@ -601,6 +729,15 @@ module MarkdownExec
601
729
  @options[:block_name] = block_name if block_name.present?
602
730
  end
603
731
 
732
+ # Formats multiline body content as a title string.
733
+ # @param body_lines [Array<String>] The lines of body content.
734
+ # @return [String] Formatted title.
735
+ def format_multiline_body_as_title(body_lines)
736
+ body_lines.map.with_index do |line, index|
737
+ index.zero? ? line : " #{line}"
738
+ end.join("\n") << "\n"
739
+ end
740
+
604
741
  ## summarize blocks
605
742
  #
606
743
  def get_block_summary(call_options, fcb)
@@ -614,9 +751,12 @@ module MarkdownExec
614
751
  else
615
752
  fcb.title
616
753
  end
617
- bm = extract_named_captures_from_option(titlexcall, opts[:block_name_match])
618
- fcb.stdin = extract_named_captures_from_option(titlexcall, opts[:block_stdin_scan])
619
- fcb.stdout = extract_named_captures_from_option(titlexcall, opts[:block_stdout_scan])
754
+ bm = extract_named_captures_from_option(titlexcall,
755
+ opts[:block_name_match])
756
+ fcb.stdin = extract_named_captures_from_option(titlexcall,
757
+ opts[:block_stdin_scan])
758
+ fcb.stdout = extract_named_captures_from_option(titlexcall,
759
+ opts[:block_stdout_scan])
620
760
 
621
761
  shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
622
762
  fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
@@ -628,22 +768,13 @@ module MarkdownExec
628
768
  fcb
629
769
  end
630
770
 
631
- ##
632
- # Handles errors that occur during the block listing process.
633
- #
634
- def handle_error(err)
635
- warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
636
- warn(caller[0..4])
637
- raise StandardError, error
638
- end
639
-
640
771
  # Handles the link-back operation.
641
772
  #
642
773
  # @param opts [Hash] Configuration options hash.
643
- # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
774
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
644
775
  def handle_back_link(opts)
645
776
  history_state_pop(opts)
646
- [LOAD_FILE, '']
777
+ [LoadFile::Load, '']
647
778
  end
648
779
 
649
780
  # Handles the execution and display of remainder blocks from a selected menu item.
@@ -651,18 +782,27 @@ module MarkdownExec
651
782
  # @param mdoc [Object] Document object containing code blocks.
652
783
  # @param opts [Hash] Configuration options hash.
653
784
  # @param selected [Hash] Selected item from the menu.
654
- # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
785
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
655
786
  # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
656
787
  def handle_remainder_blocks(mdoc, opts, selected)
657
- required_lines = collect_required_code_lines(mdoc, selected, opts: opts)
788
+ required_lines = collect_required_code_lines(mdoc, selected,
789
+ opts: opts)
658
790
  if opts[:output_script] || opts[:user_must_approve]
659
791
  display_required_code(opts, required_lines)
660
792
  end
661
- allow = opts[:user_must_approve] ? prompt_for_user_approval(opts, required_lines) : true
662
- opts[:ir_approve] = allow
663
- execute_approved_block(opts, required_lines) if opts[:ir_approve]
793
+ allow = if opts[:user_must_approve]
794
+ prompt_for_user_approval(opts,
795
+ required_lines)
796
+ else
797
+ true
798
+ end
799
+ opts[:s_ir_approve] = allow
800
+ if opts[:s_ir_approve]
801
+ execute_approved_block(opts,
802
+ required_lines)
803
+ end
664
804
 
665
- [!LOAD_FILE, '']
805
+ [LoadFile::Reuse, '']
666
806
  end
667
807
 
668
808
  # Handles the link-shell operation.
@@ -670,11 +810,11 @@ module MarkdownExec
670
810
  # @param opts [Hash] Configuration options hash.
671
811
  # @param body [Array<String>] The body content.
672
812
  # @param mdoc [Object] Document object containing code blocks.
673
- # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and a block name.
813
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
674
814
  def handle_shell_link(opts, body, mdoc)
675
815
  data = body.present? ? YAML.load(body.join("\n")) : {}
676
816
  data_file = data.fetch('file', nil)
677
- return [!LOAD_FILE, ''] unless data_file
817
+ return [LoadFile::Reuse, ''] unless data_file
678
818
 
679
819
  history_state_push(mdoc, data_file, opts)
680
820
 
@@ -682,18 +822,19 @@ module MarkdownExec
682
822
  ENV[var[0]] = var[1].to_s
683
823
  end
684
824
 
685
- [LOAD_FILE, data.fetch('block', '')]
825
+ [LoadFile::Load, data.fetch('block', '')]
686
826
  end
687
827
 
688
828
  # Handles options for the shell.
689
829
  #
690
830
  # @param opts [Hash] Configuration options hash.
691
831
  # @param selected [Hash] Selected item from the menu.
692
- # @return [Array<Symbol, String>] A tuple containing a NOT_LOAD_FILE flag and an empty string.
693
- def handle_shell_opts(opts, selected)
832
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
833
+ def handle_shell_opts(opts, selected, tgt2 = nil)
694
834
  data = YAML.load(selected[:body].join("\n"))
695
835
  data.each_key do |key|
696
- opts[key.to_sym] = value = data[key].to_s
836
+ opts[key.to_sym] = value = data[key]
837
+ tgt2[key.to_sym] = value if tgt2
697
838
  next unless opts[:menu_opts_set_format].present?
698
839
 
699
840
  print format(
@@ -702,7 +843,7 @@ module MarkdownExec
702
843
  value: value }
703
844
  ).send(opts[:menu_opts_set_color].to_sym)
704
845
  end
705
- [!LOAD_FILE, '']
846
+ [LoadFile::Reuse, '']
706
847
  end
707
848
 
708
849
  # Handles reading and processing lines from a given IO stream
@@ -712,7 +853,8 @@ module MarkdownExec
712
853
  def handle_stream(opts, stream, file_type, swap: false)
713
854
  Thread.new do
714
855
  until (line = stream.gets).nil?
715
- @execute_files[file_type] = @execute_files[file_type] + [line.strip]
856
+ @execute_files[file_type] =
857
+ @execute_files[file_type] + [line.strip]
716
858
  print line if opts[:output_stdout]
717
859
  yield line if block_given?
718
860
  end
@@ -755,7 +897,8 @@ module MarkdownExec
755
897
  #
756
898
  def initialize_and_parse_cli_options
757
899
  @options = base_options
758
- read_configuration_file!(@options, ".#{MarkdownExec::APP_NAME.downcase}.yml")
900
+ read_configuration_file!(@options,
901
+ ".#{MarkdownExec::APP_NAME.downcase}.yml")
759
902
 
760
903
  @option_parser = OptionParser.new do |opts|
761
904
  executable_name = File.basename($PROGRAM_NAME)
@@ -773,7 +916,7 @@ module MarkdownExec
773
916
  @option_parser.environment
774
917
 
775
918
  rest = @option_parser.parse!(arguments_for_mde)
776
- @options[:pass_args] = ARGV[rest.count + 1..]
919
+ @options[:s_pass_args] = ARGV[rest.count + 1..]
777
920
 
778
921
  rest
779
922
  end
@@ -783,7 +926,8 @@ module MarkdownExec
783
926
 
784
927
  @options[:logged_stdout_filename] =
785
928
  SavedAsset.stdout_name(blockname: @options[:block_name],
786
- filename: File.basename(@options[:filename], '.*'),
929
+ filename: File.basename(@options[:filename],
930
+ '.*'),
787
931
  prefix: @options[:logged_stdout_filename_prefix],
788
932
  time: Time.now.utc)
789
933
 
@@ -812,83 +956,56 @@ module MarkdownExec
812
956
 
813
957
  state = initialize_state(opts)
814
958
 
815
- # get type of messages to select
816
959
  selected_messages = yield :filter
817
960
 
818
961
  cfile.readlines(opts[:filename]).each do |line|
819
962
  next unless line
820
963
 
821
- update_line_and_block_state(line, state, opts, selected_messages, &block)
964
+ update_line_and_block_state(line, state, opts, selected_messages,
965
+ &block)
822
966
  end
823
967
  end
824
968
 
825
969
  ##
826
- # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
827
- # The list can be customized via call_options and options_block.
828
- #
829
- # @param call_options [Hash] Options passed as an argument.
830
- # @param options_block [Proc] Block for dynamic option manipulation.
831
- # @return [Array<FCB>] An array of FCB objects representing the blocks.
832
- #
833
- def list_blocks_in_file(call_options = {}, &options_block)
834
- opts = optsmerge(call_options, options_block)
835
- use_chrome = !opts[:no_chrome]
836
-
837
- blocks = []
838
- append_initial_divider(blocks, opts) if use_chrome
839
-
840
- iter_blocks_in_file(opts) do |btype, fcb|
841
- case btype
842
- when :filter
843
- filter_block_types
844
- when :line
845
- process_line_blocks(blocks, fcb, opts, use_chrome)
846
- when :blocks
847
- append_block_summary(blocks, fcb, opts)
848
- end
849
- end
850
-
851
- append_final_divider(blocks, opts) if use_chrome
852
- blocks
853
- rescue StandardError => err
854
- handle_error(err)
855
- end
856
-
857
- ##
858
- # Processes lines within the file and converts them into blocks if they match certain criteria.
859
- #
860
- def process_line_blocks(blocks, fcb, opts, use_chrome)
861
- ## convert line to block
862
- #
863
- if opts[:menu_divider_match].present? &&
864
- (mbody = fcb.body[0].match opts[:menu_divider_match])
865
- if use_chrome
866
- blocks.push FCB.new(
867
- { chrome: true,
868
- disabled: '',
869
- dname: format(opts[:menu_divider_format],
870
- mbody[:name]).send(opts[:menu_divider_color].to_sym),
871
- oname: mbody[:name] }
872
- )
873
- end
874
- elsif opts[:menu_task_match].present? &&
875
- (fcb.body[0].match opts[:menu_task_match])
876
- if use_chrome
877
- blocks.push FCB.new(
878
- { chrome: true,
879
- disabled: '',
880
- dname: format(
881
- opts[:menu_task_format],
882
- $~.named_captures.transform_keys(&:to_sym)
883
- ).send(opts[:menu_task_color].to_sym),
884
- oname: format(
885
- opts[:menu_task_format],
886
- $~.named_captures.transform_keys(&:to_sym)
887
- ) }
888
- )
889
- end
970
+ # Returns a lambda expression based on the given procname.
971
+ # @param procname [String] The name of the process to generate a lambda for.
972
+ # @param options [Hash] The options hash, necessary for some lambdas to access.
973
+ # @return [Lambda] The corresponding lambda expression.
974
+ def lambda_for_procname(procname, options)
975
+ case procname
976
+ when 'debug'
977
+ lambda { |value|
978
+ tap_config value: value
979
+ }
980
+ when 'exit'
981
+ ->(_) { exit }
982
+ when 'help'
983
+ lambda { |_|
984
+ fout menu_help
985
+ exit
986
+ }
987
+ when 'path'
988
+ ->(value) { read_configuration_file!(options, value) }
989
+ when 'show_config'
990
+ lambda { |_|
991
+ finalize_cli_argument_processing(options)
992
+ fout options.sort_by_key.to_yaml
993
+ }
994
+ when 'val_as_bool'
995
+ lambda { |value|
996
+ value.instance_of?(::String) ? (value.chomp != '0') : value
997
+ }
998
+ when 'val_as_int'
999
+ ->(value) { value.to_i }
1000
+ when 'val_as_str'
1001
+ ->(value) { value.to_s }
1002
+ when 'version'
1003
+ lambda { |_|
1004
+ fout MarkdownExec::VERSION
1005
+ exit
1006
+ }
890
1007
  else
891
- # line not added
1008
+ procname
892
1009
  end
893
1010
  end
894
1011
 
@@ -944,38 +1061,68 @@ module MarkdownExec
944
1061
  #
945
1062
  def list_named_blocks_in_file(call_options = {}, &options_block)
946
1063
  opts = optsmerge call_options, options_block
1064
+ blocks_per_opts(
1065
+ menu_from_file(opts.merge(struct: true)).select do |fcb|
1066
+ Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
1067
+ end, opts
1068
+ )
1069
+ end
1070
+
1071
+ # return true if values were modified
1072
+ # execute block once per filename
1073
+ #
1074
+ def load_auto_blocks(opts, blocks_in_file)
1075
+ return unless opts[:document_load_opts_block_name].present?
1076
+ return if opts[:s_most_recent_filename] == opts[:filename]
1077
+
1078
+ block = block_find(blocks_in_file, :oname,
1079
+ opts[:document_load_opts_block_name])
1080
+ return unless block
1081
+
1082
+ handle_shell_opts(opts, block, @options)
1083
+ opts[:s_most_recent_filename] = opts[:filename]
1084
+ true
1085
+ end
947
1086
 
948
- blocks = list_blocks_in_file(opts.merge(struct: true)).select do |fcb|
949
- # fcb.fetch(:name, '') != '' && Filter.fcb_select?(opts, fcb)
950
- Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
1087
+ def mdoc_and_menu_from_file(opts)
1088
+ menu_blocks = menu_from_file(opts.merge(struct: true))
1089
+ mdoc = MDoc.new(menu_blocks) do |nopts|
1090
+ opts.merge!(nopts)
951
1091
  end
952
- blocks_per_opts(blocks, opts)
1092
+ [menu_blocks, mdoc]
953
1093
  end
954
1094
 
955
1095
  ## Handles the file loading and returns the blocks in the file and MDoc instance
956
1096
  #
957
- def load_file_and_prepare_menu(opts)
958
- blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
959
- mdoc = MDoc.new(blocks_in_file) do |nopts|
960
- opts.merge!(nopts)
1097
+ def mdoc_menu_and_selected_from_file(opts)
1098
+ blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1099
+ if load_auto_blocks(opts, blocks_in_file)
1100
+ # recreate menu with new options
1101
+ #
1102
+ blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
961
1103
  end
1104
+
962
1105
  blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1106
+ add_menu_chrome_blocks!(blocks_menu)
963
1107
  [blocks_in_file, blocks_menu, mdoc]
964
1108
  end
965
1109
 
966
- def make_block_labels(call_options = {})
967
- opts = options.merge(call_options)
968
- list_blocks_in_file(opts).map do |fcb|
969
- BlockLabel.make(
970
- filename: opts[:filename],
971
- headings: fcb.fetch(:headings, []),
972
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
973
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
974
- title: fcb[:title],
975
- text: fcb[:text],
976
- body: fcb[:body]
977
- )
978
- end.compact
1110
+ def menu_chrome_colored_option(opts,
1111
+ option_symbol = :menu_option_back_name)
1112
+ if opts[:menu_chrome_color]
1113
+ menu_chrome_formatted_option(opts,
1114
+ option_symbol).send(opts[:menu_chrome_color].to_sym)
1115
+ else
1116
+ menu_chrome_formatted_option(opts, option_symbol)
1117
+ end
1118
+ end
1119
+
1120
+ def menu_chrome_formatted_option(opts,
1121
+ option_symbol = :menu_option_back_name)
1122
+ val1 = safeval(opts.fetch(option_symbol, ''))
1123
+ val1 unless opts[:menu_chrome_format]
1124
+
1125
+ format(opts[:menu_chrome_format], val1)
979
1126
  end
980
1127
 
981
1128
  def menu_export(data = menu_for_optparse)
@@ -995,7 +1142,13 @@ module MarkdownExec
995
1142
  when :line
996
1143
  if options[:menu_divider_match] &&
997
1144
  (mbody = fcb.body[0].match(options[:menu_divider_match]))
998
- menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
1145
+ menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name],
1146
+ disabled: '' })
1147
+ end
1148
+ if options[:menu_note_match] &&
1149
+ (mbody = fcb.body[0].match(options[:menu_note_match]))
1150
+ menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name],
1151
+ disabled: '' })
999
1152
  end
1000
1153
  when :blocks
1001
1154
  menu += [fcb.oname]
@@ -1004,57 +1157,49 @@ module MarkdownExec
1004
1157
  menu
1005
1158
  end
1006
1159
 
1007
- # :reek:DuplicateMethodCall
1008
- # :reek:NestedIterators
1160
+ ##
1161
+ # Generates a menu suitable for OptionParser from the menu items defined in YAML format.
1162
+ # @return [Array<Hash>] The array of option hashes for OptionParser.
1009
1163
  def menu_for_optparse
1010
1164
  menu_from_yaml.map do |menu_item|
1011
1165
  menu_item.merge(
1012
- {
1013
- opt_name: menu_item[:opt_name]&.to_sym,
1014
- proccode: case menu_item[:procname]
1015
- when 'debug'
1016
- lambda { |value|
1017
- tap_config value: value
1018
- }
1019
- when 'exit'
1020
- lambda { |_|
1021
- exit
1022
- }
1023
- when 'help'
1024
- lambda { |_|
1025
- fout menu_help
1026
- exit
1027
- }
1028
- when 'path'
1029
- lambda { |value|
1030
- read_configuration_file!(options, value)
1031
- }
1032
- when 'show_config'
1033
- lambda { |_|
1034
- finalize_cli_argument_processing(options)
1035
- fout options.sort_by_key.to_yaml
1036
- }
1037
- when 'val_as_bool'
1038
- lambda { |value|
1039
- value.instance_of?(::String) ? (value.chomp != '0') : value
1040
- }
1041
- when 'val_as_int'
1042
- ->(value) { value.to_i }
1043
- when 'val_as_str'
1044
- ->(value) { value.to_s }
1045
- when 'version'
1046
- lambda { |_|
1047
- fout MarkdownExec::VERSION
1048
- exit
1049
- }
1050
- else
1051
- menu_item[:procname]
1052
- end
1053
- }
1166
+ opt_name: menu_item[:opt_name]&.to_sym,
1167
+ proccode: lambda_for_procname(menu_item[:procname], options)
1054
1168
  )
1055
1169
  end
1056
1170
  end
1057
1171
 
1172
+ ##
1173
+ # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
1174
+ # The list can be customized via call_options and options_block.
1175
+ #
1176
+ # @param call_options [Hash] Options passed as an argument.
1177
+ # @param options_block [Proc] Block for dynamic option manipulation.
1178
+ # @return [Array<FCB>] An array of FCB objects representing the blocks.
1179
+ #
1180
+ def menu_from_file(call_options = {},
1181
+ &options_block)
1182
+ opts = optsmerge(call_options, options_block)
1183
+ use_chrome = !opts[:no_chrome]
1184
+
1185
+ blocks = []
1186
+ iter_blocks_in_file(opts) do |btype, fcb|
1187
+ case btype
1188
+ when :blocks
1189
+ append_block_summary(blocks, fcb, opts)
1190
+ when :filter # what btypes are responded to?
1191
+ %i[blocks line]
1192
+ when :line
1193
+ create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
1194
+ end
1195
+ end
1196
+ blocks
1197
+ rescue StandardError => err
1198
+ warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
1199
+ warn(caller[0..4])
1200
+ raise StandardError, error
1201
+ end
1202
+
1058
1203
  def menu_help
1059
1204
  @option_parser.help
1060
1205
  end
@@ -1064,7 +1209,9 @@ module MarkdownExec
1064
1209
  end
1065
1210
 
1066
1211
  def menu_option_append(opts, options, item)
1067
- return unless item[:long_name].present? || item[:short_name].present?
1212
+ unless item[:long_name].present? || item[:short_name].present?
1213
+ return
1214
+ end
1068
1215
 
1069
1216
  opts.on(*[
1070
1217
  # - long name
@@ -1077,7 +1224,9 @@ module MarkdownExec
1077
1224
 
1078
1225
  # - description and default
1079
1226
  [item[:description],
1080
- ("[#{value_for_cli item[:default]}]" if item[:default].present?)].compact.join(' '),
1227
+ (if item[:default].present?
1228
+ "[#{value_for_cli item[:default]}]"
1229
+ end)].compact.join(' '),
1081
1230
 
1082
1231
  # apply proccode, if present, to value
1083
1232
  # save value to options hash if option is named
@@ -1090,9 +1239,24 @@ module MarkdownExec
1090
1239
  ].compact)
1091
1240
  end
1092
1241
 
1242
+ def menu_with_block_labels(call_options = {})
1243
+ opts = options.merge(call_options)
1244
+ menu_from_file(opts).map do |fcb|
1245
+ BlockLabel.make(
1246
+ filename: opts[:filename],
1247
+ headings: fcb.fetch(:headings, []),
1248
+ menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1249
+ menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1250
+ title: fcb[:title],
1251
+ text: fcb[:text],
1252
+ body: fcb[:body]
1253
+ )
1254
+ end.compact
1255
+ end
1256
+
1093
1257
  def next_block_name_from_command_line_arguments(opts)
1094
- if opts[:cli_rest].present?
1095
- opts[:block_name] = opts[:cli_rest].pop
1258
+ if opts[:s_cli_rest].present?
1259
+ opts[:block_name] = opts[:s_cli_rest].pop
1096
1260
  false # repeat_menu
1097
1261
  else
1098
1262
  true # repeat_menu
@@ -1119,7 +1283,10 @@ module MarkdownExec
1119
1283
 
1120
1284
  [['Script', :saved_filespec],
1121
1285
  ['StdOut', :logged_stdout_filespec]].each do |label, name|
1122
- oq << [label, @options[name], DISPLAY_LEVEL_ADMIN] if @options[name]
1286
+ if @options[name]
1287
+ oq << [label, @options[name],
1288
+ DISPLAY_LEVEL_ADMIN]
1289
+ end
1123
1290
  end
1124
1291
 
1125
1292
  oq.map do |label, value, level|
@@ -1150,7 +1317,9 @@ module MarkdownExec
1150
1317
  def prepare_blocks_menu(blocks_in_file, opts)
1151
1318
  # next if fcb.fetch(:disabled, false)
1152
1319
  # next unless fcb.fetch(:name, '').present?
1153
- blocks_in_file.map do |fcb|
1320
+ replace_consecutive_blanks(blocks_in_file).map do |fcb|
1321
+ next if Filter.prepared_not_in_menu?(opts, fcb)
1322
+
1154
1323
  fcb.merge!(
1155
1324
  name: fcb.dname,
1156
1325
  label: BlockLabel.make(
@@ -1176,7 +1345,7 @@ module MarkdownExec
1176
1345
  fcb.oname = fcb.dname = fcb.title || ''
1177
1346
  return unless fcb.body
1178
1347
 
1179
- set_fcb_title(fcb)
1348
+ update_title_from_body(fcb)
1180
1349
 
1181
1350
  if block &&
1182
1351
  selected_messages.include?(:blocks) &&
@@ -1194,6 +1363,13 @@ module MarkdownExec
1194
1363
  block.call(:line, fcb)
1195
1364
  end
1196
1365
 
1366
+ class MenuOptions
1367
+ YES = 1
1368
+ NO = 2
1369
+ SCRIPT_TO_CLIPBOARD = 3
1370
+ SAVE_SCRIPT = 4
1371
+ end
1372
+
1197
1373
  ##
1198
1374
  # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1199
1375
  # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
@@ -1211,44 +1387,40 @@ module MarkdownExec
1211
1387
  ##
1212
1388
  def prompt_for_user_approval(opts, required_lines)
1213
1389
  # Present a selection menu for user approval.
1214
- sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
1215
- menu.default 1
1216
- menu.choice opts[:prompt_yes], 1
1217
- menu.choice opts[:prompt_no], 2
1218
- menu.choice opts[:prompt_script_to_clipboard], 3
1219
- menu.choice opts[:prompt_save_script], 4
1390
+
1391
+ sel = @prompt.select(opts[:prompt_approve_block],
1392
+ filter: true) do |menu|
1393
+ menu.default MenuOptions::YES
1394
+ menu.choice opts[:prompt_yes], MenuOptions::YES
1395
+ menu.choice opts[:prompt_no], MenuOptions::NO
1396
+ menu.choice opts[:prompt_script_to_clipboard],
1397
+ MenuOptions::SCRIPT_TO_CLIPBOARD
1398
+ menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
1220
1399
  end
1221
1400
 
1222
- if sel == 3
1401
+ if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1223
1402
  copy_to_clipboard(required_lines)
1224
- elsif sel == 4
1403
+ elsif sel == MenuOptions::SAVE_SCRIPT
1225
1404
  save_to_file(opts, required_lines)
1226
1405
  end
1227
1406
 
1228
- sel == 1
1407
+ sel == MenuOptions::YES
1408
+ rescue TTY::Reader::InputInterrupt
1409
+ exit 1
1229
1410
  end
1230
1411
 
1231
- ## insert back option at head or tail
1232
- #
1233
- ## Adds a back option at the head or tail of a menu
1234
- #
1235
- def prompt_menu_add_back(items, label)
1236
- return items unless @options[:menu_with_back] && history_state_exist?
1237
-
1238
- state = history_state_partition(@options)
1239
- @hs_curr = state[:unit]
1240
- @hs_rest = state[:rest]
1241
- @options[:menu_back_at_top] ? [label] + items : items + [label]
1242
- end
1243
-
1244
- ## insert exit option at head or tail
1245
- #
1246
- def prompt_menu_add_exit(items, label)
1247
- if @options[:menu_exit_at_top]
1248
- (@options[:menu_with_exit] ? [label] : []) + items
1249
- else
1250
- items + (@options[:menu_with_exit] ? [label] : [])
1412
+ def prompt_select_continue(opts)
1413
+ sel = @prompt.select(
1414
+ opts[:prompt_after_bash_exec],
1415
+ filter: true,
1416
+ quiet: true
1417
+ ) do |menu|
1418
+ menu.choice opts[:prompt_yes]
1419
+ menu.choice opts[:prompt_exit]
1251
1420
  end
1421
+ sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1422
+ rescue TTY::Reader::InputInterrupt
1423
+ exit 1
1252
1424
  end
1253
1425
 
1254
1426
  # :reek:UtilityFunction ### temp
@@ -1267,7 +1439,9 @@ module MarkdownExec
1267
1439
  temp_blocks = []
1268
1440
 
1269
1441
  temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
1270
- return temp_blocks if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
1442
+ if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
1443
+ return temp_blocks
1444
+ end
1271
1445
 
1272
1446
  if File.exist?(temp_blocks_file_path)
1273
1447
  temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
@@ -1276,6 +1450,24 @@ module MarkdownExec
1276
1450
  temp_blocks
1277
1451
  end
1278
1452
 
1453
+ # Replace duplicate blanks (where :oname is not present) with a single blank line.
1454
+ #
1455
+ # @param [Array<Hash>] lines Array of hashes to process.
1456
+ # @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
1457
+ def replace_consecutive_blanks(lines)
1458
+ lines.chunk_while do |i, j|
1459
+ i[:oname].to_s.empty? && j[:oname].to_s.empty?
1460
+ end.map do |chunk|
1461
+ if chunk.any? do |line|
1462
+ line[:oname].to_s.strip.empty?
1463
+ end
1464
+ chunk.first
1465
+ else
1466
+ chunk
1467
+ end
1468
+ end.flatten
1469
+ end
1470
+
1279
1471
  def run
1280
1472
  clear_required_file
1281
1473
  execute_block_with_error_handling(initialize_and_parse_cli_options)
@@ -1293,7 +1485,15 @@ module MarkdownExec
1293
1485
 
1294
1486
  saved_name_split filename
1295
1487
  @options[:save_executed_script] = false
1296
- select_approve_and_execute_block({})
1488
+ select_approve_and_execute_block
1489
+ end
1490
+
1491
+ def safeval(str)
1492
+ eval(str)
1493
+ rescue StandardError
1494
+ warn $!
1495
+ binding.pry if $tap_enable
1496
+ raise StandardError, $!
1297
1497
  end
1298
1498
 
1299
1499
  def save_to_file(opts, required_lines)
@@ -1320,40 +1520,48 @@ module MarkdownExec
1320
1520
  # @param call_options [Hash] Initial options for the method.
1321
1521
  # @param options_block [Block] Block of options to be merged with call_options.
1322
1522
  # @return [Nil] Returns nil if no code block is selected or an error occurs.
1323
- def select_approve_and_execute_block(call_options, &options_block)
1324
- opts = optsmerge(call_options, options_block)
1325
- repeat_menu = true && !opts[:block_name].present?
1326
- load_file = !LOAD_FILE
1327
- default = 1
1523
+ def select_approve_and_execute_block(call_options = {},
1524
+ &options_block)
1525
+ base_opts = optsmerge(call_options, options_block)
1526
+ repeat_menu = true && !base_opts[:block_name].present?
1527
+ load_file = LoadFile::Reuse
1528
+ default = nil
1529
+ block = nil
1328
1530
 
1329
1531
  loop do
1330
1532
  loop do
1331
- opts[:back] = false
1332
- blocks_in_file, blocks_menu, mdoc = load_file_and_prepare_menu(opts)
1333
-
1334
- unless opts[:block_name].present?
1335
- block_name, state = wait_for_user_selection(blocks_in_file, blocks_menu, default,
1336
- opts)
1337
- case state
1338
- when :exit
1339
- return nil
1340
- when :back
1341
- opts[:block_name] = block_name[:option]
1342
- opts[:back] = true
1343
- when :continue
1344
- opts[:block_name] = block_name
1345
- end
1533
+ opts = base_opts.dup
1534
+ opts[:s_back] = false
1535
+ blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
1536
+ block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
1537
+ default, opts)
1538
+ return if state == MenuState::EXIT
1539
+
1540
+ load_file, next_block_name = approve_and_execute_block(block, opts,
1541
+ mdoc)
1542
+ default = load_file == LoadFile::Load ? nil : opts[:block_name]
1543
+ base_opts[:block_name] = opts[:block_name] = next_block_name
1544
+ base_opts[:filename] = opts[:filename]
1545
+
1546
+ # user prompt to exit if the menu will be displayed again
1547
+ #
1548
+ if repeat_menu &&
1549
+ block[:shell] == BlockType::BASH &&
1550
+ opts[:pause_after_bash_exec] &&
1551
+ prompt_select_continue(opts) == MenuState::EXIT
1552
+ return
1346
1553
  end
1347
1554
 
1348
- load_file, next_block_name = approve_and_execute_block(opts, mdoc)
1349
- default = load_file == LOAD_FILE ? 1 : opts[:block_name]
1350
- opts[:block_name] = next_block_name
1351
- break if state == :continue && load_file == LOAD_FILE
1555
+ # exit current document/menu if loading next document or single block_name was specified
1556
+ #
1557
+ if state == MenuState::CONTINUE && load_file == LoadFile::Load
1558
+ break
1559
+ end
1352
1560
  break unless repeat_menu
1353
1561
  end
1354
- break if load_file != LOAD_FILE
1562
+ break if load_file == LoadFile::Reuse
1355
1563
 
1356
- repeat_menu = next_block_name_from_command_line_arguments(opts)
1564
+ repeat_menu = next_block_name_from_command_line_arguments(base_opts)
1357
1565
  end
1358
1566
  rescue StandardError => err
1359
1567
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1383,21 +1591,24 @@ module MarkdownExec
1383
1591
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1384
1592
  def select_option_with_metadata(prompt_text, items, opts = {})
1385
1593
  selection = @prompt.select(prompt_text,
1386
- prompt_menu_add_exit(
1387
- prompt_menu_add_back(
1388
- items,
1389
- opts[:menu_option_back_name]
1390
- ),
1391
- opts[:menu_option_exit_name]
1392
- ),
1594
+ items,
1393
1595
  opts.merge(filter: true))
1394
- if selection == opts[:menu_option_back_name]
1395
- { option: selection, curr: @hs_curr, rest: @hs_rest, shell: BLOCK_TYPE_LINK }
1396
- elsif selection == opts[:menu_option_exit_name]
1397
- { option: selection }
1398
- else
1399
- { selected: selection }
1400
- end
1596
+
1597
+ items.find { |item| item[:dname] == selection }
1598
+ .merge(
1599
+ if selection == menu_chrome_colored_option(opts,
1600
+ :menu_option_back_name)
1601
+ { option: selection, curr: @hs_curr, rest: @hs_rest,
1602
+ shell: BlockType::LINK }
1603
+ elsif selection == menu_chrome_colored_option(opts,
1604
+ :menu_option_exit_name)
1605
+ { option: selection }
1606
+ else
1607
+ { selected: selection }
1608
+ end
1609
+ )
1610
+ rescue TTY::Reader::InputInterrupt
1611
+ exit 1
1401
1612
  end
1402
1613
 
1403
1614
  def select_recent_output
@@ -1429,21 +1640,13 @@ module MarkdownExec
1429
1640
 
1430
1641
  saved_name_split(filename)
1431
1642
 
1432
- select_approve_and_execute_block({
1433
- bash: true,
1643
+ select_approve_and_execute_block({ bash: true,
1434
1644
  save_executed_script: false,
1435
- struct: true
1436
- })
1437
- end
1438
-
1439
- # set the title of an FCB object based on its body if it is nil or empty
1440
- def set_fcb_title(fcb)
1441
- return unless fcb.title.nil? || fcb.title.empty?
1442
-
1443
- fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
1645
+ struct: true })
1444
1646
  end
1445
1647
 
1446
- def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
1648
+ def start_fenced_block(opts, line, headings,
1649
+ fenced_start_extended_regex)
1447
1650
  fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1448
1651
  rest = fcb_title_groups.fetch(:rest, '')
1449
1652
 
@@ -1476,7 +1679,11 @@ module MarkdownExec
1476
1679
  end
1477
1680
 
1478
1681
  def tty_prompt_without_disabled_symbol
1479
- TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
1682
+ TTY::Prompt.new(interrupt: lambda {
1683
+ puts;
1684
+ raise TTY::Reader::InputInterrupt
1685
+ },
1686
+ symbols: { cross: ' ' })
1480
1687
  end
1481
1688
 
1482
1689
  ##
@@ -1523,7 +1730,8 @@ module MarkdownExec
1523
1730
  #
1524
1731
  # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1525
1732
  ##
1526
- def update_line_and_block_state(line, state, opts, selected_messages, &block)
1733
+ def update_line_and_block_state(line, state, opts, selected_messages,
1734
+ &block)
1527
1735
  if opts[:menu_blocks_with_headings]
1528
1736
  state[:headings] =
1529
1737
  update_document_headings(line, state[:headings], opts)
@@ -1531,7 +1739,8 @@ module MarkdownExec
1531
1739
 
1532
1740
  if line.match(state[:fenced_start_and_end_regex])
1533
1741
  if state[:in_fenced_block]
1534
- process_fenced_block(state[:fcb], opts, selected_messages, &block)
1742
+ process_fenced_block(state[:fcb], opts, selected_messages,
1743
+ &block)
1535
1744
  state[:in_fenced_block] = false
1536
1745
  else
1537
1746
  state[:fcb] =
@@ -1557,26 +1766,59 @@ module MarkdownExec
1557
1766
  @options
1558
1767
  end
1559
1768
 
1560
- ## Handles the menu interaction and returns selected block name and option state
1769
+ # Updates the title of an FCB object from its body content if the title is nil or empty.
1770
+ def update_title_from_body(fcb)
1771
+ return unless fcb.title.nil? || fcb.title.empty?
1772
+
1773
+ fcb.title = derive_title_from_body(fcb)
1774
+ end
1775
+
1776
+ def wait_for_user_selected_block(blocks_in_file, blocks_menu,
1777
+ default, opts)
1778
+ block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
1779
+ default, opts)
1780
+ case state
1781
+ when MenuState::BACK
1782
+ opts[:block_name] = block[:dname]
1783
+ opts[:s_back] = true
1784
+ when MenuState::CONTINUE
1785
+ opts[:block_name] = block[:dname]
1786
+ end
1787
+
1788
+ [block, state]
1789
+ end
1790
+
1791
+ ## Handles the menu interaction and returns selected block and option state
1561
1792
  #
1562
- def wait_for_user_selection(blocks_in_file, blocks_menu, default, opts)
1793
+ def wait_for_user_selection(blocks_in_file, blocks_menu, default,
1794
+ opts)
1563
1795
  pt = opts[:prompt_select_block].to_s
1564
1796
  bm = prepare_blocks_menu(blocks_menu, opts)
1565
- return [nil, :exit] if bm.count.zero?
1797
+ return [nil, MenuState::EXIT] if bm.count.zero?
1798
+
1799
+ o2 = if default
1800
+ opts.merge(default: default)
1801
+ else
1802
+ opts
1803
+ end
1566
1804
 
1567
- obj = select_option_with_metadata(pt, bm, opts.merge(
1568
- default: default,
1805
+ obj = select_option_with_metadata(pt, bm, o2.merge(
1569
1806
  per_page: opts[:select_page_height]
1570
1807
  ))
1571
- case obj.fetch(:option, nil)
1572
- when opts[:menu_option_exit_name]
1573
- [nil, :exit]
1574
- when opts[:menu_option_back_name]
1575
- [obj, :back]
1808
+
1809
+ case obj.fetch(:oname, nil)
1810
+ when menu_chrome_formatted_option(opts, :menu_option_exit_name)
1811
+ [nil, MenuState::EXIT]
1812
+ when menu_chrome_formatted_option(opts, :menu_option_back_name)
1813
+ [obj, MenuState::BACK]
1576
1814
  else
1577
- label_block = blocks_in_file.find { |fcb| fcb.dname == obj[:selected] }
1578
- [label_block.oname, :continue]
1815
+ [obj, MenuState::CONTINUE]
1579
1816
  end
1817
+ rescue StandardError => err
1818
+ warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
1819
+ warn caller.take(3)
1820
+ binding.pry if $tap_enable
1821
+ raise ArgumentError, error
1580
1822
  end
1581
1823
 
1582
1824
  # Handles the core logic for generating the command file's metadata and content.
@@ -1593,7 +1835,8 @@ module MarkdownExec
1593
1835
 
1594
1836
  @execute_script_filespec =
1595
1837
  @options[:saved_filespec] =
1596
- File.join opts[:saved_script_folder], opts[:saved_script_filename]
1838
+ File.join opts[:saved_script_folder],
1839
+ opts[:saved_script_filename]
1597
1840
 
1598
1841
  shebang = if @options[:shebang]&.present?
1599
1842
  "#{@options[:shebang]} #{@options[:shell]}\n"
@@ -1656,7 +1899,7 @@ if $PROGRAM_NAME == __FILE__
1656
1899
 
1657
1900
  def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1658
1901
  pigeon = 'E'
1659
- obj = { pass_args: pigeon }
1902
+ obj = { s_pass_args: pigeon }
1660
1903
 
1661
1904
  c = MarkdownExec::MarkParse.new
1662
1905
 
@@ -1676,15 +1919,17 @@ if $PROGRAM_NAME == __FILE__
1676
1919
  end
1677
1920
 
1678
1921
  def test_set_fcb_title
1679
- # sample input and output data for testing set_fcb_title method
1922
+ # sample input and output data for testing update_title_from_body method
1680
1923
  input_output_data = [
1681
1924
  {
1682
1925
  input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1683
1926
  output: "puts 'Hello, world!'"
1684
1927
  },
1685
1928
  {
1686
- input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
1687
- output: 'def add(x, y) x + y end'
1929
+ input: FCB.new(title: '',
1930
+ body: ['def add(x, y)',
1931
+ ' x + y', 'end']),
1932
+ output: "def add(x, y)\n x + y\n end\n"
1688
1933
  },
1689
1934
  {
1690
1935
  input: FCB.new(title: 'foo', body: %w[bar baz]),
@@ -1697,10 +1942,20 @@ if $PROGRAM_NAME == __FILE__
1697
1942
  input_output_data.each do |data|
1698
1943
  input = data[:input]
1699
1944
  output = data[:output]
1700
- @mark_parse.set_fcb_title(input)
1945
+ @mark_parse.update_title_from_body(input)
1701
1946
  assert_equal output, input.title
1702
1947
  end
1703
1948
  end
1704
1949
  end
1705
- end
1706
- end
1950
+
1951
+ def test_select_block
1952
+ blocks = [block1, block2]
1953
+ menu = [m1, m2]
1954
+
1955
+ block, state = obj.select_block(blocks, menu, nil, {})
1956
+
1957
+ assert_equal block1, block
1958
+ assert_equal MenuState::CONTINUE, state
1959
+ end
1960
+ end # module MarkdownExec
1961
+ end # if