markdown_exec 1.4.1 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
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
242
292
 
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
- })
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
+ )
301
+
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,57 @@ 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
+ match_criteria = [
542
+ { match: :menu_task_match, format: :menu_task_format,
543
+ color: :menu_task_color },
544
+ { match: :menu_divider_match, format: :menu_divider_format,
545
+ color: :menu_divider_color },
546
+ { match: :menu_note_match, format: :menu_note_format,
547
+ color: :menu_note_color }
548
+ ]
549
+
550
+ match_criteria.each do |criteria|
551
+ unless opts[criteria[:match]].present? &&
552
+ (mbody = fcb.body[0].match opts[criteria[:match]])
553
+ next
554
+ end
555
+
556
+ create_and_add_chrome_block(blocks, fcb, mbody, opts[criteria[:format]],
557
+ opts[criteria[:color]].to_sym)
558
+ break
559
+ end
560
+ end
561
+
562
+ def create_and_write_file_with_permissions(file_path, content,
563
+ chmod_value)
438
564
  dirname = File.dirname(file_path)
439
565
  FileUtils.mkdir_p dirname
440
566
  File.write(file_path, content)
@@ -450,13 +576,29 @@ module MarkdownExec
450
576
  def delete_required_temp_file
451
577
  temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
452
578
 
453
- return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
579
+ if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
580
+ return
581
+ end
454
582
 
455
583
  FileUtils.rm_f(temp_blocks_file_path)
456
584
 
457
585
  clear_required_file
458
586
  end
459
587
 
588
+ # Derives a title from the body of an FCB object.
589
+ # @param fcb [Object] The FCB object whose title is to be derived.
590
+ # @return [String] The derived title.
591
+ def derive_title_from_body(fcb)
592
+ body_content = fcb&.body
593
+ return '' unless body_content
594
+
595
+ if body_content.count == 1
596
+ body_content.first
597
+ else
598
+ format_multiline_body_as_title(body_content)
599
+ end
600
+ end
601
+
460
602
  ## Determines the correct filename to use for searching files
461
603
  #
462
604
  def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
@@ -464,7 +606,8 @@ module MarkdownExec
464
606
  if specified_filename&.present?
465
607
  return specified_filename if specified_filename.start_with?('/')
466
608
 
467
- File.join(specified_folder || default_folder, specified_filename)
609
+ File.join(specified_folder || default_folder,
610
+ specified_filename)
468
611
  elsif specified_folder&.present?
469
612
  File.join(specified_folder,
470
613
  filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
@@ -486,7 +629,7 @@ module MarkdownExec
486
629
  command_execute(
487
630
  opts,
488
631
  required_lines.flatten.join("\n"),
489
- args: opts.fetch(:pass_args, [])
632
+ args: opts.fetch(:s_pass_args, [])
490
633
  )
491
634
  initialize_and_save_execution_output
492
635
  output_execution_summary
@@ -496,20 +639,22 @@ module MarkdownExec
496
639
  # Reports and executes block logic
497
640
  def execute_block_logic(files)
498
641
  @options[:filename] = select_document_if_multiple(files)
499
- select_approve_and_execute_block({
500
- bash: true,
501
- struct: true
502
- })
642
+ select_approve_and_execute_block({ bash: true,
643
+ struct: true })
503
644
  end
504
645
 
505
646
  ## Executes the block specified in the options
506
647
  #
507
648
  def execute_block_with_error_handling(rest)
508
649
  finalize_cli_argument_processing(rest)
509
- @options[:cli_rest] = rest
650
+ @options[:s_cli_rest] = rest
510
651
  execute_code_block_based_on_options(@options)
511
652
  rescue FileMissingError => err
512
653
  puts "File missing: #{err}"
654
+ rescue StandardError => err
655
+ warn(error = "ERROR ** MarkParse.execute_block_with_error_handling(); #{err.inspect}")
656
+ binding.pry if $tap_enable
657
+ raise ArgumentError, error
513
658
  end
514
659
 
515
660
  # Main method to execute a block based on options and block_name
@@ -521,7 +666,8 @@ module MarkdownExec
521
666
  doc_glob: -> { fout options[:md_filename_glob] },
522
667
  list_blocks: lambda do
523
668
  fout_list (files.map do |file|
524
- make_block_labels(filename: file, struct: true)
669
+ menu_with_block_labels(filename: file,
670
+ struct: true)
525
671
  end).flatten(1)
526
672
  end,
527
673
  list_default_yaml: -> { fout_list list_default_yaml },
@@ -571,15 +717,6 @@ module MarkdownExec
571
717
  false
572
718
  end
573
719
 
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
720
  ## post-parse options configuration
584
721
  #
585
722
  def finalize_cli_argument_processing(rest)
@@ -601,6 +738,16 @@ module MarkdownExec
601
738
  @options[:block_name] = block_name if block_name.present?
602
739
  end
603
740
 
741
+ # Formats multiline body content as a title string.
742
+ # indents all but first line with two spaces so it displays correctly in menu
743
+ # @param body_lines [Array<String>] The lines of body content.
744
+ # @return [String] Formatted title.
745
+ def format_multiline_body_as_title(body_lines)
746
+ body_lines.map.with_index do |line, index|
747
+ index.zero? ? line : " #{line}"
748
+ end.join("\n") << "\n"
749
+ end
750
+
604
751
  ## summarize blocks
605
752
  #
606
753
  def get_block_summary(call_options, fcb)
@@ -614,9 +761,12 @@ module MarkdownExec
614
761
  else
615
762
  fcb.title
616
763
  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])
764
+ bm = extract_named_captures_from_option(titlexcall,
765
+ opts[:block_name_match])
766
+ fcb.stdin = extract_named_captures_from_option(titlexcall,
767
+ opts[:block_stdin_scan])
768
+ fcb.stdout = extract_named_captures_from_option(titlexcall,
769
+ opts[:block_stdout_scan])
620
770
 
621
771
  shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
622
772
  fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
@@ -628,22 +778,13 @@ module MarkdownExec
628
778
  fcb
629
779
  end
630
780
 
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
781
  # Handles the link-back operation.
641
782
  #
642
783
  # @param opts [Hash] Configuration options hash.
643
- # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
784
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
644
785
  def handle_back_link(opts)
645
786
  history_state_pop(opts)
646
- [LOAD_FILE, '']
787
+ [LoadFile::Load, '']
647
788
  end
648
789
 
649
790
  # Handles the execution and display of remainder blocks from a selected menu item.
@@ -651,18 +792,27 @@ module MarkdownExec
651
792
  # @param mdoc [Object] Document object containing code blocks.
652
793
  # @param opts [Hash] Configuration options hash.
653
794
  # @param selected [Hash] Selected item from the menu.
654
- # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
795
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
655
796
  # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
656
797
  def handle_remainder_blocks(mdoc, opts, selected)
657
- required_lines = collect_required_code_lines(mdoc, selected, opts: opts)
798
+ required_lines = collect_required_code_lines(mdoc, selected,
799
+ opts: opts)
658
800
  if opts[:output_script] || opts[:user_must_approve]
659
801
  display_required_code(opts, required_lines)
660
802
  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]
803
+ allow = if opts[:user_must_approve]
804
+ prompt_for_user_approval(opts,
805
+ required_lines)
806
+ else
807
+ true
808
+ end
809
+ opts[:s_ir_approve] = allow
810
+ if opts[:s_ir_approve]
811
+ execute_approved_block(opts,
812
+ required_lines)
813
+ end
664
814
 
665
- [!LOAD_FILE, '']
815
+ [LoadFile::Reuse, '']
666
816
  end
667
817
 
668
818
  # Handles the link-shell operation.
@@ -670,11 +820,11 @@ module MarkdownExec
670
820
  # @param opts [Hash] Configuration options hash.
671
821
  # @param body [Array<String>] The body content.
672
822
  # @param mdoc [Object] Document object containing code blocks.
673
- # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and a block name.
823
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
674
824
  def handle_shell_link(opts, body, mdoc)
675
825
  data = body.present? ? YAML.load(body.join("\n")) : {}
676
826
  data_file = data.fetch('file', nil)
677
- return [!LOAD_FILE, ''] unless data_file
827
+ return [LoadFile::Reuse, ''] unless data_file
678
828
 
679
829
  history_state_push(mdoc, data_file, opts)
680
830
 
@@ -682,18 +832,19 @@ module MarkdownExec
682
832
  ENV[var[0]] = var[1].to_s
683
833
  end
684
834
 
685
- [LOAD_FILE, data.fetch('block', '')]
835
+ [LoadFile::Load, data.fetch('block', '')]
686
836
  end
687
837
 
688
838
  # Handles options for the shell.
689
839
  #
690
840
  # @param opts [Hash] Configuration options hash.
691
841
  # @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)
842
+ # @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
843
+ def handle_shell_opts(opts, selected, tgt2 = nil)
694
844
  data = YAML.load(selected[:body].join("\n"))
695
845
  data.each_key do |key|
696
- opts[key.to_sym] = value = data[key].to_s
846
+ opts[key.to_sym] = value = data[key]
847
+ tgt2[key.to_sym] = value if tgt2
697
848
  next unless opts[:menu_opts_set_format].present?
698
849
 
699
850
  print format(
@@ -702,7 +853,7 @@ module MarkdownExec
702
853
  value: value }
703
854
  ).send(opts[:menu_opts_set_color].to_sym)
704
855
  end
705
- [!LOAD_FILE, '']
856
+ [LoadFile::Reuse, '']
706
857
  end
707
858
 
708
859
  # Handles reading and processing lines from a given IO stream
@@ -712,7 +863,8 @@ module MarkdownExec
712
863
  def handle_stream(opts, stream, file_type, swap: false)
713
864
  Thread.new do
714
865
  until (line = stream.gets).nil?
715
- @execute_files[file_type] = @execute_files[file_type] + [line.strip]
866
+ @execute_files[file_type] =
867
+ @execute_files[file_type] + [line.strip]
716
868
  print line if opts[:output_stdout]
717
869
  yield line if block_given?
718
870
  end
@@ -751,11 +903,22 @@ module MarkdownExec
751
903
  ENV[MDE_HISTORY_ENV_NAME] = new_history
752
904
  end
753
905
 
906
+ # Indents all lines in a given string with a specified indentation string.
907
+ # @param body [String] A multi-line string to be indented.
908
+ # @param indent [String] The string used for indentation (default is an empty string).
909
+ # @return [String] A single string with each line indented as specified.
910
+ def indent_all_lines(body, indent = nil)
911
+ return body if !indent.present?
912
+
913
+ body.lines.map { |line| indent + line.chomp }.join("\n")
914
+ end
915
+
754
916
  ## Sets up the options and returns the parsed arguments
755
917
  #
756
918
  def initialize_and_parse_cli_options
757
919
  @options = base_options
758
- read_configuration_file!(@options, ".#{MarkdownExec::APP_NAME.downcase}.yml")
920
+ read_configuration_file!(@options,
921
+ ".#{MarkdownExec::APP_NAME.downcase}.yml")
759
922
 
760
923
  @option_parser = OptionParser.new do |opts|
761
924
  executable_name = File.basename($PROGRAM_NAME)
@@ -773,7 +936,7 @@ module MarkdownExec
773
936
  @option_parser.environment
774
937
 
775
938
  rest = @option_parser.parse!(arguments_for_mde)
776
- @options[:pass_args] = ARGV[rest.count + 1..]
939
+ @options[:s_pass_args] = ARGV[rest.count + 1..]
777
940
 
778
941
  rest
779
942
  end
@@ -783,7 +946,8 @@ module MarkdownExec
783
946
 
784
947
  @options[:logged_stdout_filename] =
785
948
  SavedAsset.stdout_name(blockname: @options[:block_name],
786
- filename: File.basename(@options[:filename], '.*'),
949
+ filename: File.basename(@options[:filename],
950
+ '.*'),
787
951
  prefix: @options[:logged_stdout_filename_prefix],
788
952
  time: Time.now.utc)
789
953
 
@@ -812,83 +976,56 @@ module MarkdownExec
812
976
 
813
977
  state = initialize_state(opts)
814
978
 
815
- # get type of messages to select
816
979
  selected_messages = yield :filter
817
980
 
818
981
  cfile.readlines(opts[:filename]).each do |line|
819
982
  next unless line
820
983
 
821
- update_line_and_block_state(line, state, opts, selected_messages, &block)
984
+ update_line_and_block_state(line, state, opts, selected_messages,
985
+ &block)
822
986
  end
823
987
  end
824
988
 
825
989
  ##
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
990
+ # Returns a lambda expression based on the given procname.
991
+ # @param procname [String] The name of the process to generate a lambda for.
992
+ # @param options [Hash] The options hash, necessary for some lambdas to access.
993
+ # @return [Lambda] The corresponding lambda expression.
994
+ def lambda_for_procname(procname, options)
995
+ case procname
996
+ when 'debug'
997
+ lambda { |value|
998
+ tap_config value: value
999
+ }
1000
+ when 'exit'
1001
+ ->(_) { exit }
1002
+ when 'help'
1003
+ lambda { |_|
1004
+ fout menu_help
1005
+ exit
1006
+ }
1007
+ when 'path'
1008
+ ->(value) { read_configuration_file!(options, value) }
1009
+ when 'show_config'
1010
+ lambda { |_|
1011
+ finalize_cli_argument_processing(options)
1012
+ fout options.sort_by_key.to_yaml
1013
+ }
1014
+ when 'val_as_bool'
1015
+ lambda { |value|
1016
+ value.instance_of?(::String) ? (value.chomp != '0') : value
1017
+ }
1018
+ when 'val_as_int'
1019
+ ->(value) { value.to_i }
1020
+ when 'val_as_str'
1021
+ ->(value) { value.to_s }
1022
+ when 'version'
1023
+ lambda { |_|
1024
+ fout MarkdownExec::VERSION
1025
+ exit
1026
+ }
890
1027
  else
891
- # line not added
1028
+ procname
892
1029
  end
893
1030
  end
894
1031
 
@@ -944,38 +1081,68 @@ module MarkdownExec
944
1081
  #
945
1082
  def list_named_blocks_in_file(call_options = {}, &options_block)
946
1083
  opts = optsmerge call_options, options_block
1084
+ blocks_per_opts(
1085
+ menu_from_file(opts.merge(struct: true)).select do |fcb|
1086
+ Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
1087
+ end, opts
1088
+ )
1089
+ end
1090
+
1091
+ # return true if values were modified
1092
+ # execute block once per filename
1093
+ #
1094
+ def load_auto_blocks(opts, blocks_in_file)
1095
+ return unless opts[:document_load_opts_block_name].present?
1096
+ return if opts[:s_most_recent_filename] == opts[:filename]
947
1097
 
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)
1098
+ block = block_find(blocks_in_file, :oname,
1099
+ opts[:document_load_opts_block_name])
1100
+ return unless block
1101
+
1102
+ handle_shell_opts(opts, block, @options)
1103
+ opts[:s_most_recent_filename] = opts[:filename]
1104
+ true
1105
+ end
1106
+
1107
+ def mdoc_and_menu_from_file(opts)
1108
+ menu_blocks = menu_from_file(opts.merge(struct: true))
1109
+ mdoc = MDoc.new(menu_blocks) do |nopts|
1110
+ opts.merge!(nopts)
951
1111
  end
952
- blocks_per_opts(blocks, opts)
1112
+ [menu_blocks, mdoc]
953
1113
  end
954
1114
 
955
1115
  ## Handles the file loading and returns the blocks in the file and MDoc instance
956
1116
  #
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)
1117
+ def mdoc_menu_and_selected_from_file(opts)
1118
+ blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1119
+ if load_auto_blocks(opts, blocks_in_file)
1120
+ # recreate menu with new options
1121
+ #
1122
+ blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
961
1123
  end
1124
+
962
1125
  blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1126
+ add_menu_chrome_blocks!(blocks_menu)
963
1127
  [blocks_in_file, blocks_menu, mdoc]
964
1128
  end
965
1129
 
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
1130
+ def menu_chrome_colored_option(opts,
1131
+ option_symbol = :menu_option_back_name)
1132
+ if opts[:menu_chrome_color]
1133
+ menu_chrome_formatted_option(opts,
1134
+ option_symbol).send(opts[:menu_chrome_color].to_sym)
1135
+ else
1136
+ menu_chrome_formatted_option(opts, option_symbol)
1137
+ end
1138
+ end
1139
+
1140
+ def menu_chrome_formatted_option(opts,
1141
+ option_symbol = :menu_option_back_name)
1142
+ val1 = safeval(opts.fetch(option_symbol, ''))
1143
+ val1 unless opts[:menu_chrome_format]
1144
+
1145
+ format(opts[:menu_chrome_format], val1)
979
1146
  end
980
1147
 
981
1148
  def menu_export(data = menu_for_optparse)
@@ -985,76 +1152,49 @@ module MarkdownExec
985
1152
  end.to_yaml
986
1153
  end
987
1154
 
988
- def menu_for_blocks(menu_options)
989
- options = calculated_options.merge menu_options
990
- menu = []
991
- iter_blocks_in_file(options) do |btype, fcb|
992
- case btype
993
- when :filter
994
- %i[blocks line]
995
- when :line
996
- if options[:menu_divider_match] &&
997
- (mbody = fcb.body[0].match(options[:menu_divider_match]))
998
- menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
999
- end
1000
- when :blocks
1001
- menu += [fcb.oname]
1002
- end
1003
- end
1004
- menu
1005
- end
1006
-
1007
- # :reek:DuplicateMethodCall
1008
- # :reek:NestedIterators
1155
+ ##
1156
+ # Generates a menu suitable for OptionParser from the menu items defined in YAML format.
1157
+ # @return [Array<Hash>] The array of option hashes for OptionParser.
1009
1158
  def menu_for_optparse
1010
1159
  menu_from_yaml.map do |menu_item|
1011
1160
  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
- }
1161
+ opt_name: menu_item[:opt_name]&.to_sym,
1162
+ proccode: lambda_for_procname(menu_item[:procname], options)
1054
1163
  )
1055
1164
  end
1056
1165
  end
1057
1166
 
1167
+ ##
1168
+ # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
1169
+ # The list can be customized via call_options and options_block.
1170
+ #
1171
+ # @param call_options [Hash] Options passed as an argument.
1172
+ # @param options_block [Proc] Block for dynamic option manipulation.
1173
+ # @return [Array<FCB>] An array of FCB objects representing the blocks.
1174
+ #
1175
+ def menu_from_file(call_options = {},
1176
+ &options_block)
1177
+ opts = optsmerge(call_options, options_block)
1178
+ use_chrome = !opts[:no_chrome]
1179
+
1180
+ blocks = []
1181
+ iter_blocks_in_file(opts) do |btype, fcb|
1182
+ case btype
1183
+ when :blocks
1184
+ append_block_summary(blocks, fcb, opts)
1185
+ when :filter # what btypes are responded to?
1186
+ %i[blocks line]
1187
+ when :line
1188
+ create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
1189
+ end
1190
+ end
1191
+ blocks
1192
+ rescue StandardError => err
1193
+ warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
1194
+ warn(caller[0..4])
1195
+ raise StandardError, error
1196
+ end
1197
+
1058
1198
  def menu_help
1059
1199
  @option_parser.help
1060
1200
  end
@@ -1064,7 +1204,9 @@ module MarkdownExec
1064
1204
  end
1065
1205
 
1066
1206
  def menu_option_append(opts, options, item)
1067
- return unless item[:long_name].present? || item[:short_name].present?
1207
+ unless item[:long_name].present? || item[:short_name].present?
1208
+ return
1209
+ end
1068
1210
 
1069
1211
  opts.on(*[
1070
1212
  # - long name
@@ -1077,7 +1219,9 @@ module MarkdownExec
1077
1219
 
1078
1220
  # - description and default
1079
1221
  [item[:description],
1080
- ("[#{value_for_cli item[:default]}]" if item[:default].present?)].compact.join(' '),
1222
+ (if item[:default].present?
1223
+ "[#{value_for_cli item[:default]}]"
1224
+ end)].compact.join(' '),
1081
1225
 
1082
1226
  # apply proccode, if present, to value
1083
1227
  # save value to options hash if option is named
@@ -1090,9 +1234,24 @@ module MarkdownExec
1090
1234
  ].compact)
1091
1235
  end
1092
1236
 
1237
+ def menu_with_block_labels(call_options = {})
1238
+ opts = options.merge(call_options)
1239
+ menu_from_file(opts).map do |fcb|
1240
+ BlockLabel.make(
1241
+ filename: opts[:filename],
1242
+ headings: fcb.fetch(:headings, []),
1243
+ menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1244
+ menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1245
+ title: fcb[:title],
1246
+ text: fcb[:text],
1247
+ body: fcb[:body]
1248
+ )
1249
+ end.compact
1250
+ end
1251
+
1093
1252
  def next_block_name_from_command_line_arguments(opts)
1094
- if opts[:cli_rest].present?
1095
- opts[:block_name] = opts[:cli_rest].pop
1253
+ if opts[:s_cli_rest].present?
1254
+ opts[:block_name] = opts[:s_cli_rest].pop
1096
1255
  false # repeat_menu
1097
1256
  else
1098
1257
  true # repeat_menu
@@ -1119,7 +1278,10 @@ module MarkdownExec
1119
1278
 
1120
1279
  [['Script', :saved_filespec],
1121
1280
  ['StdOut', :logged_stdout_filespec]].each do |label, name|
1122
- oq << [label, @options[name], DISPLAY_LEVEL_ADMIN] if @options[name]
1281
+ if @options[name]
1282
+ oq << [label, @options[name],
1283
+ DISPLAY_LEVEL_ADMIN]
1284
+ end
1123
1285
  end
1124
1286
 
1125
1287
  oq.map do |label, value, level|
@@ -1150,9 +1312,11 @@ module MarkdownExec
1150
1312
  def prepare_blocks_menu(blocks_in_file, opts)
1151
1313
  # next if fcb.fetch(:disabled, false)
1152
1314
  # next unless fcb.fetch(:name, '').present?
1153
- blocks_in_file.map do |fcb|
1315
+ replace_consecutive_blanks(blocks_in_file).map do |fcb|
1316
+ next if Filter.prepared_not_in_menu?(opts, fcb)
1317
+
1154
1318
  fcb.merge!(
1155
- name: fcb.dname,
1319
+ name: indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)),
1156
1320
  label: BlockLabel.make(
1157
1321
  body: fcb[:body],
1158
1322
  filename: opts[:filename],
@@ -1176,7 +1340,7 @@ module MarkdownExec
1176
1340
  fcb.oname = fcb.dname = fcb.title || ''
1177
1341
  return unless fcb.body
1178
1342
 
1179
- set_fcb_title(fcb)
1343
+ update_title_from_body(fcb)
1180
1344
 
1181
1345
  if block &&
1182
1346
  selected_messages.include?(:blocks) &&
@@ -1194,6 +1358,13 @@ module MarkdownExec
1194
1358
  block.call(:line, fcb)
1195
1359
  end
1196
1360
 
1361
+ class MenuOptions
1362
+ YES = 1
1363
+ NO = 2
1364
+ SCRIPT_TO_CLIPBOARD = 3
1365
+ SAVE_SCRIPT = 4
1366
+ end
1367
+
1197
1368
  ##
1198
1369
  # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1199
1370
  # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
@@ -1211,44 +1382,40 @@ module MarkdownExec
1211
1382
  ##
1212
1383
  def prompt_for_user_approval(opts, required_lines)
1213
1384
  # 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
1385
+
1386
+ sel = @prompt.select(opts[:prompt_approve_block],
1387
+ filter: true) do |menu|
1388
+ menu.default MenuOptions::YES
1389
+ menu.choice opts[:prompt_yes], MenuOptions::YES
1390
+ menu.choice opts[:prompt_no], MenuOptions::NO
1391
+ menu.choice opts[:prompt_script_to_clipboard],
1392
+ MenuOptions::SCRIPT_TO_CLIPBOARD
1393
+ menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
1220
1394
  end
1221
1395
 
1222
- if sel == 3
1396
+ if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1223
1397
  copy_to_clipboard(required_lines)
1224
- elsif sel == 4
1398
+ elsif sel == MenuOptions::SAVE_SCRIPT
1225
1399
  save_to_file(opts, required_lines)
1226
1400
  end
1227
1401
 
1228
- sel == 1
1402
+ sel == MenuOptions::YES
1403
+ rescue TTY::Reader::InputInterrupt
1404
+ exit 1
1229
1405
  end
1230
1406
 
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] : [])
1407
+ def prompt_select_continue(opts)
1408
+ sel = @prompt.select(
1409
+ opts[:prompt_after_bash_exec],
1410
+ filter: true,
1411
+ quiet: true
1412
+ ) do |menu|
1413
+ menu.choice opts[:prompt_yes]
1414
+ menu.choice opts[:prompt_exit]
1251
1415
  end
1416
+ sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1417
+ rescue TTY::Reader::InputInterrupt
1418
+ exit 1
1252
1419
  end
1253
1420
 
1254
1421
  # :reek:UtilityFunction ### temp
@@ -1267,7 +1434,9 @@ module MarkdownExec
1267
1434
  temp_blocks = []
1268
1435
 
1269
1436
  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?
1437
+ if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
1438
+ return temp_blocks
1439
+ end
1271
1440
 
1272
1441
  if File.exist?(temp_blocks_file_path)
1273
1442
  temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
@@ -1276,6 +1445,24 @@ module MarkdownExec
1276
1445
  temp_blocks
1277
1446
  end
1278
1447
 
1448
+ # Replace duplicate blanks (where :oname is not present) with a single blank line.
1449
+ #
1450
+ # @param [Array<Hash>] lines Array of hashes to process.
1451
+ # @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
1452
+ def replace_consecutive_blanks(lines)
1453
+ lines.chunk_while do |i, j|
1454
+ i[:oname].to_s.empty? && j[:oname].to_s.empty?
1455
+ end.map do |chunk|
1456
+ if chunk.any? do |line|
1457
+ line[:oname].to_s.strip.empty?
1458
+ end
1459
+ chunk.first
1460
+ else
1461
+ chunk
1462
+ end
1463
+ end.flatten
1464
+ end
1465
+
1279
1466
  def run
1280
1467
  clear_required_file
1281
1468
  execute_block_with_error_handling(initialize_and_parse_cli_options)
@@ -1293,7 +1480,15 @@ module MarkdownExec
1293
1480
 
1294
1481
  saved_name_split filename
1295
1482
  @options[:save_executed_script] = false
1296
- select_approve_and_execute_block({})
1483
+ select_approve_and_execute_block
1484
+ end
1485
+
1486
+ def safeval(str)
1487
+ eval(str)
1488
+ rescue StandardError
1489
+ warn $!
1490
+ binding.pry if $tap_enable
1491
+ raise StandardError, $!
1297
1492
  end
1298
1493
 
1299
1494
  def save_to_file(opts, required_lines)
@@ -1320,40 +1515,48 @@ module MarkdownExec
1320
1515
  # @param call_options [Hash] Initial options for the method.
1321
1516
  # @param options_block [Block] Block of options to be merged with call_options.
1322
1517
  # @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
1518
+ def select_approve_and_execute_block(call_options = {},
1519
+ &options_block)
1520
+ base_opts = optsmerge(call_options, options_block)
1521
+ repeat_menu = true && !base_opts[:block_name].present?
1522
+ load_file = LoadFile::Reuse
1523
+ default = nil
1524
+ block = nil
1328
1525
 
1329
1526
  loop do
1330
1527
  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
1528
+ opts = base_opts.dup
1529
+ opts[:s_back] = false
1530
+ blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
1531
+ block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
1532
+ default, opts)
1533
+ return if state == MenuState::EXIT
1534
+
1535
+ load_file, next_block_name = approve_and_execute_block(block, opts,
1536
+ mdoc)
1537
+ default = load_file == LoadFile::Load ? nil : opts[:block_name]
1538
+ base_opts[:block_name] = opts[:block_name] = next_block_name
1539
+ base_opts[:filename] = opts[:filename]
1540
+
1541
+ # user prompt to exit if the menu will be displayed again
1542
+ #
1543
+ if repeat_menu &&
1544
+ block[:shell] == BlockType::BASH &&
1545
+ opts[:pause_after_bash_exec] &&
1546
+ prompt_select_continue(opts) == MenuState::EXIT
1547
+ return
1346
1548
  end
1347
1549
 
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
1550
+ # exit current document/menu if loading next document or single block_name was specified
1551
+ #
1552
+ if state == MenuState::CONTINUE && load_file == LoadFile::Load
1553
+ break
1554
+ end
1352
1555
  break unless repeat_menu
1353
1556
  end
1354
- break if load_file != LOAD_FILE
1557
+ break if load_file == LoadFile::Reuse
1355
1558
 
1356
- repeat_menu = next_block_name_from_command_line_arguments(opts)
1559
+ repeat_menu = next_block_name_from_command_line_arguments(base_opts)
1357
1560
  end
1358
1561
  rescue StandardError => err
1359
1562
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1383,21 +1586,24 @@ module MarkdownExec
1383
1586
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1384
1587
  def select_option_with_metadata(prompt_text, items, opts = {})
1385
1588
  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
- ),
1589
+ items,
1393
1590
  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
1591
+
1592
+ items.find { |item| item[:dname] == selection }
1593
+ .merge(
1594
+ if selection == menu_chrome_colored_option(opts,
1595
+ :menu_option_back_name)
1596
+ { option: selection, curr: @hs_curr, rest: @hs_rest,
1597
+ shell: BlockType::LINK }
1598
+ elsif selection == menu_chrome_colored_option(opts,
1599
+ :menu_option_exit_name)
1600
+ { option: selection }
1601
+ else
1602
+ { selected: selection }
1603
+ end
1604
+ )
1605
+ rescue TTY::Reader::InputInterrupt
1606
+ exit 1
1401
1607
  end
1402
1608
 
1403
1609
  def select_recent_output
@@ -1429,27 +1635,20 @@ module MarkdownExec
1429
1635
 
1430
1636
  saved_name_split(filename)
1431
1637
 
1432
- select_approve_and_execute_block({
1433
- bash: true,
1638
+ select_approve_and_execute_block({ bash: true,
1434
1639
  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]
1640
+ struct: true })
1444
1641
  end
1445
1642
 
1446
- def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
1643
+ def start_fenced_block(opts, line, headings,
1644
+ fenced_start_extended_regex)
1447
1645
  fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1448
1646
  rest = fcb_title_groups.fetch(:rest, '')
1449
1647
 
1450
1648
  fcb = FCB.new
1451
1649
  fcb.headings = headings
1452
1650
  fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
1651
+ fcb.indent = fcb_title_groups.fetch(:indent, '')
1453
1652
  fcb.shell = fcb_title_groups.fetch(:shell, '')
1454
1653
  fcb.title = fcb_title_groups.fetch(:name, '')
1455
1654
  fcb.body = []
@@ -1476,7 +1675,11 @@ module MarkdownExec
1476
1675
  end
1477
1676
 
1478
1677
  def tty_prompt_without_disabled_symbol
1479
- TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
1678
+ TTY::Prompt.new(interrupt: lambda {
1679
+ puts;
1680
+ raise TTY::Reader::InputInterrupt
1681
+ },
1682
+ symbols: { cross: ' ' })
1480
1683
  end
1481
1684
 
1482
1685
  ##
@@ -1523,7 +1726,8 @@ module MarkdownExec
1523
1726
  #
1524
1727
  # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1525
1728
  ##
1526
- def update_line_and_block_state(line, state, opts, selected_messages, &block)
1729
+ def update_line_and_block_state(line, state, opts, selected_messages,
1730
+ &block)
1527
1731
  if opts[:menu_blocks_with_headings]
1528
1732
  state[:headings] =
1529
1733
  update_document_headings(line, state[:headings], opts)
@@ -1531,7 +1735,8 @@ module MarkdownExec
1531
1735
 
1532
1736
  if line.match(state[:fenced_start_and_end_regex])
1533
1737
  if state[:in_fenced_block]
1534
- process_fenced_block(state[:fcb], opts, selected_messages, &block)
1738
+ process_fenced_block(state[:fcb], opts, selected_messages,
1739
+ &block)
1535
1740
  state[:in_fenced_block] = false
1536
1741
  else
1537
1742
  state[:fcb] =
@@ -1540,7 +1745,13 @@ module MarkdownExec
1540
1745
  state[:in_fenced_block] = true
1541
1746
  end
1542
1747
  elsif state[:in_fenced_block] && state[:fcb].body
1543
- state[:fcb].body += [line.chomp]
1748
+ ## add line to fenced code block
1749
+ # remove fcb indent if possible
1750
+ #
1751
+ state[:fcb].body += [
1752
+ line.chomp.sub(/^#{state[:fcb].indent}/, '')
1753
+ ]
1754
+
1544
1755
  else
1545
1756
  process_line(line, opts, selected_messages, &block)
1546
1757
  end
@@ -1557,26 +1768,59 @@ module MarkdownExec
1557
1768
  @options
1558
1769
  end
1559
1770
 
1560
- ## Handles the menu interaction and returns selected block name and option state
1771
+ # Updates the title of an FCB object from its body content if the title is nil or empty.
1772
+ def update_title_from_body(fcb)
1773
+ return unless fcb.title.nil? || fcb.title.empty?
1774
+
1775
+ fcb.title = derive_title_from_body(fcb)
1776
+ end
1777
+
1778
+ def wait_for_user_selected_block(blocks_in_file, blocks_menu,
1779
+ default, opts)
1780
+ block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
1781
+ default, opts)
1782
+ case state
1783
+ when MenuState::BACK
1784
+ opts[:block_name] = block[:dname]
1785
+ opts[:s_back] = true
1786
+ when MenuState::CONTINUE
1787
+ opts[:block_name] = block[:dname]
1788
+ end
1789
+
1790
+ [block, state]
1791
+ end
1792
+
1793
+ ## Handles the menu interaction and returns selected block and option state
1561
1794
  #
1562
- def wait_for_user_selection(blocks_in_file, blocks_menu, default, opts)
1795
+ def wait_for_user_selection(blocks_in_file, blocks_menu, default,
1796
+ opts)
1563
1797
  pt = opts[:prompt_select_block].to_s
1564
1798
  bm = prepare_blocks_menu(blocks_menu, opts)
1565
- return [nil, :exit] if bm.count.zero?
1799
+ return [nil, MenuState::EXIT] if bm.count.zero?
1566
1800
 
1567
- obj = select_option_with_metadata(pt, bm, opts.merge(
1568
- default: default,
1801
+ o2 = if default
1802
+ opts.merge(default: default)
1803
+ else
1804
+ opts
1805
+ end
1806
+
1807
+ obj = select_option_with_metadata(pt, bm, o2.merge(
1569
1808
  per_page: opts[:select_page_height]
1570
1809
  ))
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]
1810
+
1811
+ case obj.fetch(:oname, nil)
1812
+ when menu_chrome_formatted_option(opts, :menu_option_exit_name)
1813
+ [nil, MenuState::EXIT]
1814
+ when menu_chrome_formatted_option(opts, :menu_option_back_name)
1815
+ [obj, MenuState::BACK]
1576
1816
  else
1577
- label_block = blocks_in_file.find { |fcb| fcb.dname == obj[:selected] }
1578
- [label_block.oname, :continue]
1817
+ [obj, MenuState::CONTINUE]
1579
1818
  end
1819
+ rescue StandardError => err
1820
+ warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
1821
+ warn caller.take(3)
1822
+ binding.pry if $tap_enable
1823
+ raise ArgumentError, error
1580
1824
  end
1581
1825
 
1582
1826
  # Handles the core logic for generating the command file's metadata and content.
@@ -1593,7 +1837,8 @@ module MarkdownExec
1593
1837
 
1594
1838
  @execute_script_filespec =
1595
1839
  @options[:saved_filespec] =
1596
- File.join opts[:saved_script_folder], opts[:saved_script_filename]
1840
+ File.join opts[:saved_script_folder],
1841
+ opts[:saved_script_filename]
1597
1842
 
1598
1843
  shebang = if @options[:shebang]&.present?
1599
1844
  "#{@options[:shebang]} #{@options[:shell]}\n"
@@ -1656,7 +1901,7 @@ if $PROGRAM_NAME == __FILE__
1656
1901
 
1657
1902
  def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1658
1903
  pigeon = 'E'
1659
- obj = { pass_args: pigeon }
1904
+ obj = { s_pass_args: pigeon }
1660
1905
 
1661
1906
  c = MarkdownExec::MarkParse.new
1662
1907
 
@@ -1676,15 +1921,17 @@ if $PROGRAM_NAME == __FILE__
1676
1921
  end
1677
1922
 
1678
1923
  def test_set_fcb_title
1679
- # sample input and output data for testing set_fcb_title method
1924
+ # sample input and output data for testing update_title_from_body method
1680
1925
  input_output_data = [
1681
1926
  {
1682
1927
  input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1683
1928
  output: "puts 'Hello, world!'"
1684
1929
  },
1685
1930
  {
1686
- input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
1687
- output: 'def add(x, y) x + y end'
1931
+ input: FCB.new(title: '',
1932
+ body: ['def add(x, y)',
1933
+ ' x + y', 'end']),
1934
+ output: "def add(x, y)\n x + y\n end\n"
1688
1935
  },
1689
1936
  {
1690
1937
  input: FCB.new(title: 'foo', body: %w[bar baz]),
@@ -1697,10 +1944,20 @@ if $PROGRAM_NAME == __FILE__
1697
1944
  input_output_data.each do |data|
1698
1945
  input = data[:input]
1699
1946
  output = data[:output]
1700
- @mark_parse.set_fcb_title(input)
1947
+ @mark_parse.update_title_from_body(input)
1701
1948
  assert_equal output, input.title
1702
1949
  end
1703
1950
  end
1704
1951
  end
1705
- end
1706
- end
1952
+
1953
+ def test_select_block
1954
+ blocks = [block1, block2]
1955
+ menu = [m1, m2]
1956
+
1957
+ block, state = obj.select_block(blocks, menu, nil, {})
1958
+
1959
+ assert_equal block1, block
1960
+ assert_equal MenuState::CONTINUE, state
1961
+ end
1962
+ end # module MarkdownExec
1963
+ end # if