markdown_exec 1.4.1 → 1.5

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