markdown_exec 1.4 → 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
@@ -9,6 +9,7 @@ require 'fileutils'
9
9
  require 'open3'
10
10
  require 'optparse'
11
11
  require 'shellwords'
12
+ require 'tmpdir'
12
13
  require 'tty-prompt'
13
14
  require 'yaml'
14
15
 
@@ -37,10 +38,6 @@ $stdout.sync = true
37
38
 
38
39
  MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
39
40
 
40
- # macros
41
- #
42
- LOAD_FILE = true
43
-
44
41
  # custom error: file specified is missing
45
42
  #
46
43
  class FileMissingError < StandardError; end
@@ -62,6 +59,17 @@ class Hash
62
59
  end
63
60
  end
64
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
+
65
73
  # integer value for comparison
66
74
  #
67
75
  def options_fetch_display_level(options)
@@ -109,16 +117,23 @@ def dp(str)
109
117
  lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
110
118
  end
111
119
 
120
+ def rpry
121
+ require 'pry-nav'
122
+ require 'pry-stack_explorer'
123
+ end
124
+
112
125
  public
113
126
 
114
127
  # :reek:UtilityFunction
115
- 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)
116
130
  SavedFilesMatcher.most_recent_list(saved_stdout_folder,
117
131
  saved_stdout_glob, list_count)
118
132
  end
119
133
 
120
134
  # :reek:UtilityFunction
121
- 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)
122
137
  SavedFilesMatcher.most_recent_list(saved_script_folder,
123
138
  saved_script_glob, list_count)
124
139
  end
@@ -173,10 +188,10 @@ module MarkdownExec
173
188
  FNR12 = ',~'
174
189
 
175
190
  SHELL_COLOR_OPTIONS = {
176
- BLOCK_TYPE_BASH => :menu_bash_color,
177
- BLOCK_TYPE_LINK => :menu_link_color,
178
- BLOCK_TYPE_OPTS => :menu_opts_color,
179
- 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
180
195
  }.freeze
181
196
 
182
197
  ##
@@ -208,6 +223,22 @@ module MarkdownExec
208
223
  @prompt = tty_prompt_without_disabled_symbol
209
224
  end
210
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
+
211
242
  ##
212
243
  # Appends a summary of a block (FCB) to the blocks array.
213
244
  #
@@ -217,38 +248,58 @@ module MarkdownExec
217
248
  blocks.push get_block_summary(opts, fcb)
218
249
  end
219
250
 
220
- ##
221
- # 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
222
252
  #
223
- def append_final_divider(blocks, opts)
224
- 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
225
267
 
226
- blocks.push FCB.new(
227
- { chrome: true,
228
- disabled: '',
229
- dname: format(opts[:menu_divider_format],
230
- opts[:menu_final_divider])
231
- .send(opts[:menu_divider_color].to_sym),
232
- 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
233
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
234
281
  end
235
282
 
236
- ##
237
- # Appends an initial divider to the blocks array if it is specified in options.
238
- #
239
- def append_initial_divider(blocks, opts)
240
- 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
241
292
 
242
- blocks.push FCB.new({
243
- # name: '',
244
- chrome: true,
245
- dname: format(
246
- opts[:menu_divider_format],
247
- opts[:menu_initial_divider]
248
- ).send(opts[:menu_divider_color].to_sym),
249
- oname: opts[:menu_initial_divider],
250
- disabled: '' # __LINE__.to_s
251
- })
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)
252
303
  end
253
304
 
254
305
  # Execute a code block after approval and provide user interaction options.
@@ -260,14 +311,12 @@ module MarkdownExec
260
311
  # @param opts [Hash] Options hash containing configuration settings.
261
312
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
262
313
  #
263
- def approve_and_execute_block(opts, mdoc)
264
- selected = mdoc.get_block_by_name(opts[:block_name])
265
-
266
- if selected.fetch(:shell, '') == BLOCK_TYPE_LINK
314
+ def approve_and_execute_block(selected, opts, mdoc)
315
+ if selected.fetch(:shell, '') == BlockType::LINK
267
316
  handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
268
- elsif opts.fetch(:back, false)
317
+ elsif opts.fetch(:s_back, false)
269
318
  handle_back_link(opts)
270
- elsif selected[:shell] == BLOCK_TYPE_OPTS
319
+ elsif selected[:shell] == BlockType::OPTS
271
320
  handle_shell_opts(opts, selected)
272
321
  else
273
322
  handle_remainder_blocks(mdoc, opts, selected)
@@ -301,10 +350,23 @@ module MarkdownExec
301
350
  env_str(item[:env_var],
302
351
  default: OptionValue.for_hash(item_default))
303
352
  end
304
- [item[:opt_name], item[:proccode] ? item[:proccode].call(value) : value]
353
+ [item[:opt_name],
354
+ item[:proccode] ? item[:proccode].call(value) : value]
305
355
  end.compact.to_h
306
356
  end
307
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
+
308
370
  def blocks_per_opts(blocks, opts)
309
371
  return blocks if opts[:struct]
310
372
 
@@ -346,7 +408,7 @@ module MarkdownExec
346
408
  # @return [Array<String>] Required code blocks as an array of lines.
347
409
  def collect_required_code_lines(mdoc, selected, opts: {})
348
410
  # Apply hash in opts block to environment variables
349
- if selected[:shell] == BLOCK_TYPE_VARS
411
+ if selected[:shell] == BlockType::VARS
350
412
  data = YAML.load(selected[:body].join("\n"))
351
413
  data.each_key do |key|
352
414
  ENV[key] = value = data[key].to_s
@@ -360,7 +422,8 @@ module MarkdownExec
360
422
  end
361
423
  end
362
424
 
363
- required = mdoc.collect_recursively_required_code(opts[:block_name], opts: opts)
425
+ required = mdoc.collect_recursively_required_code(opts[:block_name],
426
+ opts: opts)
364
427
  read_required_blocks_from_temp_file + required[:code]
365
428
  end
366
429
 
@@ -416,6 +479,20 @@ module MarkdownExec
416
479
  fout "Error ENOENT: #{err.inspect}"
417
480
  end
418
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
+
419
496
  def copy_to_clipboard(required_lines)
420
497
  text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
421
498
  Clipboard.copy(text)
@@ -433,7 +510,48 @@ module MarkdownExec
433
510
  cnt / 2
434
511
  end
435
512
 
436
- 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)
437
555
  dirname = File.dirname(file_path)
438
556
  FileUtils.mkdir_p dirname
439
557
  File.write(file_path, content)
@@ -449,13 +567,29 @@ module MarkdownExec
449
567
  def delete_required_temp_file
450
568
  temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
451
569
 
452
- 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
453
573
 
454
574
  FileUtils.rm_f(temp_blocks_file_path)
455
575
 
456
576
  clear_required_file
457
577
  end
458
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
+
459
593
  ## Determines the correct filename to use for searching files
460
594
  #
461
595
  def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
@@ -463,7 +597,8 @@ module MarkdownExec
463
597
  if specified_filename&.present?
464
598
  return specified_filename if specified_filename.start_with?('/')
465
599
 
466
- File.join(specified_folder || default_folder, specified_filename)
600
+ File.join(specified_folder || default_folder,
601
+ specified_filename)
467
602
  elsif specified_folder&.present?
468
603
  File.join(specified_folder,
469
604
  filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
@@ -485,7 +620,7 @@ module MarkdownExec
485
620
  command_execute(
486
621
  opts,
487
622
  required_lines.flatten.join("\n"),
488
- args: opts.fetch(:pass_args, [])
623
+ args: opts.fetch(:s_pass_args, [])
489
624
  )
490
625
  initialize_and_save_execution_output
491
626
  output_execution_summary
@@ -495,23 +630,26 @@ module MarkdownExec
495
630
  # Reports and executes block logic
496
631
  def execute_block_logic(files)
497
632
  @options[:filename] = select_document_if_multiple(files)
498
- select_approve_and_execute_block({
499
- bash: true,
500
- struct: true
501
- })
633
+ select_approve_and_execute_block({ bash: true,
634
+ struct: true })
502
635
  end
503
636
 
504
637
  ## Executes the block specified in the options
505
638
  #
506
639
  def execute_block_with_error_handling(rest)
507
640
  finalize_cli_argument_processing(rest)
508
- execute_code_block_based_on_options(@options, @options[:block_name])
641
+ @options[:s_cli_rest] = rest
642
+ execute_code_block_based_on_options(@options)
509
643
  rescue FileMissingError => err
510
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
511
649
  end
512
650
 
513
651
  # Main method to execute a block based on options and block_name
514
- def execute_code_block_based_on_options(options, _block_name = '')
652
+ def execute_code_block_based_on_options(options)
515
653
  options = calculated_options.merge(options)
516
654
  update_options(options, over: false)
517
655
 
@@ -519,7 +657,8 @@ module MarkdownExec
519
657
  doc_glob: -> { fout options[:md_filename_glob] },
520
658
  list_blocks: lambda do
521
659
  fout_list (files.map do |file|
522
- make_block_labels(filename: file, struct: true)
660
+ menu_with_block_labels(filename: file,
661
+ struct: true)
523
662
  end).flatten(1)
524
663
  end,
525
664
  list_default_yaml: -> { fout_list list_default_yaml },
@@ -569,15 +708,6 @@ module MarkdownExec
569
708
  false
570
709
  end
571
710
 
572
- ##
573
- # Determines the types of blocks to select based on the filter.
574
- #
575
- def filter_block_types
576
- ## return type of blocks to select
577
- #
578
- %i[blocks line]
579
- end
580
-
581
711
  ## post-parse options configuration
582
712
  #
583
713
  def finalize_cli_argument_processing(rest)
@@ -599,6 +729,15 @@ module MarkdownExec
599
729
  @options[:block_name] = block_name if block_name.present?
600
730
  end
601
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
+
602
741
  ## summarize blocks
603
742
  #
604
743
  def get_block_summary(call_options, fcb)
@@ -612,9 +751,12 @@ module MarkdownExec
612
751
  else
613
752
  fcb.title
614
753
  end
615
- bm = extract_named_captures_from_option(titlexcall, opts[:block_name_match])
616
- fcb.stdin = extract_named_captures_from_option(titlexcall, opts[:block_stdin_scan])
617
- 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])
618
760
 
619
761
  shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
620
762
  fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
@@ -626,22 +768,13 @@ module MarkdownExec
626
768
  fcb
627
769
  end
628
770
 
629
- ##
630
- # Handles errors that occur during the block listing process.
631
- #
632
- def handle_error(err)
633
- warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
634
- warn(caller[0..4])
635
- raise StandardError, error
636
- end
637
-
638
771
  # Handles the link-back operation.
639
772
  #
640
773
  # @param opts [Hash] Configuration options hash.
641
- # @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.
642
775
  def handle_back_link(opts)
643
776
  history_state_pop(opts)
644
- [LOAD_FILE, '']
777
+ [LoadFile::Load, '']
645
778
  end
646
779
 
647
780
  # Handles the execution and display of remainder blocks from a selected menu item.
@@ -649,18 +782,27 @@ module MarkdownExec
649
782
  # @param mdoc [Object] Document object containing code blocks.
650
783
  # @param opts [Hash] Configuration options hash.
651
784
  # @param selected [Hash] Selected item from the menu.
652
- # @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.
653
786
  # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
654
787
  def handle_remainder_blocks(mdoc, opts, selected)
655
- required_lines = collect_required_code_lines(mdoc, selected, opts: opts)
788
+ required_lines = collect_required_code_lines(mdoc, selected,
789
+ opts: opts)
656
790
  if opts[:output_script] || opts[:user_must_approve]
657
791
  display_required_code(opts, required_lines)
658
792
  end
659
- allow = opts[:user_must_approve] ? prompt_for_user_approval(opts, required_lines) : true
660
- opts[:ir_approve] = allow
661
- 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
662
804
 
663
- [!LOAD_FILE, '']
805
+ [LoadFile::Reuse, '']
664
806
  end
665
807
 
666
808
  # Handles the link-shell operation.
@@ -668,11 +810,11 @@ module MarkdownExec
668
810
  # @param opts [Hash] Configuration options hash.
669
811
  # @param body [Array<String>] The body content.
670
812
  # @param mdoc [Object] Document object containing code blocks.
671
- # @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.
672
814
  def handle_shell_link(opts, body, mdoc)
673
815
  data = body.present? ? YAML.load(body.join("\n")) : {}
674
816
  data_file = data.fetch('file', nil)
675
- return [!LOAD_FILE, ''] unless data_file
817
+ return [LoadFile::Reuse, ''] unless data_file
676
818
 
677
819
  history_state_push(mdoc, data_file, opts)
678
820
 
@@ -680,18 +822,19 @@ module MarkdownExec
680
822
  ENV[var[0]] = var[1].to_s
681
823
  end
682
824
 
683
- [LOAD_FILE, data.fetch('block', '')]
825
+ [LoadFile::Load, data.fetch('block', '')]
684
826
  end
685
827
 
686
828
  # Handles options for the shell.
687
829
  #
688
830
  # @param opts [Hash] Configuration options hash.
689
831
  # @param selected [Hash] Selected item from the menu.
690
- # @return [Array<Symbol, String>] A tuple containing a NOT_LOAD_FILE flag and an empty string.
691
- 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)
692
834
  data = YAML.load(selected[:body].join("\n"))
693
835
  data.each_key do |key|
694
- 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
695
838
  next unless opts[:menu_opts_set_format].present?
696
839
 
697
840
  print format(
@@ -700,7 +843,7 @@ module MarkdownExec
700
843
  value: value }
701
844
  ).send(opts[:menu_opts_set_color].to_sym)
702
845
  end
703
- [!LOAD_FILE, '']
846
+ [LoadFile::Reuse, '']
704
847
  end
705
848
 
706
849
  # Handles reading and processing lines from a given IO stream
@@ -710,7 +853,8 @@ module MarkdownExec
710
853
  def handle_stream(opts, stream, file_type, swap: false)
711
854
  Thread.new do
712
855
  until (line = stream.gets).nil?
713
- @execute_files[file_type] = @execute_files[file_type] + [line.strip]
856
+ @execute_files[file_type] =
857
+ @execute_files[file_type] + [line.strip]
714
858
  print line if opts[:output_stdout]
715
859
  yield line if block_given?
716
860
  end
@@ -753,7 +897,8 @@ module MarkdownExec
753
897
  #
754
898
  def initialize_and_parse_cli_options
755
899
  @options = base_options
756
- read_configuration_file!(@options, ".#{MarkdownExec::APP_NAME.downcase}.yml")
900
+ read_configuration_file!(@options,
901
+ ".#{MarkdownExec::APP_NAME.downcase}.yml")
757
902
 
758
903
  @option_parser = OptionParser.new do |opts|
759
904
  executable_name = File.basename($PROGRAM_NAME)
@@ -771,7 +916,7 @@ module MarkdownExec
771
916
  @option_parser.environment
772
917
 
773
918
  rest = @option_parser.parse!(arguments_for_mde)
774
- @options[:pass_args] = ARGV[rest.count + 1..]
919
+ @options[:s_pass_args] = ARGV[rest.count + 1..]
775
920
 
776
921
  rest
777
922
  end
@@ -781,7 +926,8 @@ module MarkdownExec
781
926
 
782
927
  @options[:logged_stdout_filename] =
783
928
  SavedAsset.stdout_name(blockname: @options[:block_name],
784
- filename: File.basename(@options[:filename], '.*'),
929
+ filename: File.basename(@options[:filename],
930
+ '.*'),
785
931
  prefix: @options[:logged_stdout_filename_prefix],
786
932
  time: Time.now.utc)
787
933
 
@@ -810,83 +956,56 @@ module MarkdownExec
810
956
 
811
957
  state = initialize_state(opts)
812
958
 
813
- # get type of messages to select
814
959
  selected_messages = yield :filter
815
960
 
816
961
  cfile.readlines(opts[:filename]).each do |line|
817
962
  next unless line
818
963
 
819
- update_line_and_block_state(line, state, opts, selected_messages, &block)
964
+ update_line_and_block_state(line, state, opts, selected_messages,
965
+ &block)
820
966
  end
821
967
  end
822
968
 
823
969
  ##
824
- # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
825
- # The list can be customized via call_options and options_block.
826
- #
827
- # @param call_options [Hash] Options passed as an argument.
828
- # @param options_block [Proc] Block for dynamic option manipulation.
829
- # @return [Array<FCB>] An array of FCB objects representing the blocks.
830
- #
831
- def list_blocks_in_file(call_options = {}, &options_block)
832
- opts = optsmerge(call_options, options_block)
833
- use_chrome = !opts[:no_chrome]
834
-
835
- blocks = []
836
- append_initial_divider(blocks, opts) if use_chrome
837
-
838
- iter_blocks_in_file(opts) do |btype, fcb|
839
- case btype
840
- when :filter
841
- filter_block_types
842
- when :line
843
- process_line_blocks(blocks, fcb, opts, use_chrome)
844
- when :blocks
845
- append_block_summary(blocks, fcb, opts)
846
- end
847
- end
848
-
849
- append_final_divider(blocks, opts) if use_chrome
850
- blocks
851
- rescue StandardError => err
852
- handle_error(err)
853
- end
854
-
855
- ##
856
- # Processes lines within the file and converts them into blocks if they match certain criteria.
857
- #
858
- def process_line_blocks(blocks, fcb, opts, use_chrome)
859
- ## convert line to block
860
- #
861
- if opts[:menu_divider_match].present? &&
862
- (mbody = fcb.body[0].match opts[:menu_divider_match])
863
- if use_chrome
864
- blocks.push FCB.new(
865
- { chrome: true,
866
- disabled: '',
867
- dname: format(opts[:menu_divider_format],
868
- mbody[:name]).send(opts[:menu_divider_color].to_sym),
869
- oname: mbody[:name] }
870
- )
871
- end
872
- elsif opts[:menu_task_match].present? &&
873
- (fcb.body[0].match opts[:menu_task_match])
874
- if use_chrome
875
- blocks.push FCB.new(
876
- { chrome: true,
877
- disabled: '',
878
- dname: format(
879
- opts[:menu_task_format],
880
- $~.named_captures.transform_keys(&:to_sym)
881
- ).send(opts[:menu_task_color].to_sym),
882
- oname: format(
883
- opts[:menu_task_format],
884
- $~.named_captures.transform_keys(&:to_sym)
885
- ) }
886
- )
887
- 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
+ }
888
1007
  else
889
- # line not added
1008
+ procname
890
1009
  end
891
1010
  end
892
1011
 
@@ -942,38 +1061,68 @@ module MarkdownExec
942
1061
  #
943
1062
  def list_named_blocks_in_file(call_options = {}, &options_block)
944
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
945
1081
 
946
- blocks = list_blocks_in_file(opts.merge(struct: true)).select do |fcb|
947
- # fcb.fetch(:name, '') != '' && Filter.fcb_select?(opts, fcb)
948
- Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
1082
+ handle_shell_opts(opts, block, @options)
1083
+ opts[:s_most_recent_filename] = opts[:filename]
1084
+ true
1085
+ end
1086
+
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)
949
1091
  end
950
- blocks_per_opts(blocks, opts)
1092
+ [menu_blocks, mdoc]
951
1093
  end
952
1094
 
953
1095
  ## Handles the file loading and returns the blocks in the file and MDoc instance
954
1096
  #
955
- def load_file_and_prepare_menu(opts)
956
- blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
957
- mdoc = MDoc.new(blocks_in_file) do |nopts|
958
- 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)
959
1103
  end
1104
+
960
1105
  blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1106
+ add_menu_chrome_blocks!(blocks_menu)
961
1107
  [blocks_in_file, blocks_menu, mdoc]
962
1108
  end
963
1109
 
964
- def make_block_labels(call_options = {})
965
- opts = options.merge(call_options)
966
- list_blocks_in_file(opts).map do |fcb|
967
- BlockLabel.make(
968
- filename: opts[:filename],
969
- headings: fcb.fetch(:headings, []),
970
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
971
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
972
- title: fcb[:title],
973
- text: fcb[:text],
974
- body: fcb[:body]
975
- )
976
- 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)
977
1126
  end
978
1127
 
979
1128
  def menu_export(data = menu_for_optparse)
@@ -993,7 +1142,13 @@ module MarkdownExec
993
1142
  when :line
994
1143
  if options[:menu_divider_match] &&
995
1144
  (mbody = fcb.body[0].match(options[:menu_divider_match]))
996
- 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: '' })
997
1152
  end
998
1153
  when :blocks
999
1154
  menu += [fcb.oname]
@@ -1002,57 +1157,49 @@ module MarkdownExec
1002
1157
  menu
1003
1158
  end
1004
1159
 
1005
- # :reek:DuplicateMethodCall
1006
- # :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.
1007
1163
  def menu_for_optparse
1008
1164
  menu_from_yaml.map do |menu_item|
1009
1165
  menu_item.merge(
1010
- {
1011
- opt_name: menu_item[:opt_name]&.to_sym,
1012
- proccode: case menu_item[:procname]
1013
- when 'debug'
1014
- lambda { |value|
1015
- tap_config value: value
1016
- }
1017
- when 'exit'
1018
- lambda { |_|
1019
- exit
1020
- }
1021
- when 'help'
1022
- lambda { |_|
1023
- fout menu_help
1024
- exit
1025
- }
1026
- when 'path'
1027
- lambda { |value|
1028
- read_configuration_file!(options, value)
1029
- }
1030
- when 'show_config'
1031
- lambda { |_|
1032
- finalize_cli_argument_processing(options)
1033
- fout options.sort_by_key.to_yaml
1034
- }
1035
- when 'val_as_bool'
1036
- lambda { |value|
1037
- value.instance_of?(::String) ? (value.chomp != '0') : value
1038
- }
1039
- when 'val_as_int'
1040
- ->(value) { value.to_i }
1041
- when 'val_as_str'
1042
- ->(value) { value.to_s }
1043
- when 'version'
1044
- lambda { |_|
1045
- fout MarkdownExec::VERSION
1046
- exit
1047
- }
1048
- else
1049
- menu_item[:procname]
1050
- end
1051
- }
1166
+ opt_name: menu_item[:opt_name]&.to_sym,
1167
+ proccode: lambda_for_procname(menu_item[:procname], options)
1052
1168
  )
1053
1169
  end
1054
1170
  end
1055
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
+
1056
1203
  def menu_help
1057
1204
  @option_parser.help
1058
1205
  end
@@ -1062,7 +1209,9 @@ module MarkdownExec
1062
1209
  end
1063
1210
 
1064
1211
  def menu_option_append(opts, options, item)
1065
- return unless item[:long_name].present? || item[:short_name].present?
1212
+ unless item[:long_name].present? || item[:short_name].present?
1213
+ return
1214
+ end
1066
1215
 
1067
1216
  opts.on(*[
1068
1217
  # - long name
@@ -1075,7 +1224,9 @@ module MarkdownExec
1075
1224
 
1076
1225
  # - description and default
1077
1226
  [item[:description],
1078
- ("[#{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(' '),
1079
1230
 
1080
1231
  # apply proccode, if present, to value
1081
1232
  # save value to options hash if option is named
@@ -1088,6 +1239,30 @@ module MarkdownExec
1088
1239
  ].compact)
1089
1240
  end
1090
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
+
1257
+ def next_block_name_from_command_line_arguments(opts)
1258
+ if opts[:s_cli_rest].present?
1259
+ opts[:block_name] = opts[:s_cli_rest].pop
1260
+ false # repeat_menu
1261
+ else
1262
+ true # repeat_menu
1263
+ end
1264
+ end
1265
+
1091
1266
  # :reek:ControlParameter
1092
1267
  def optsmerge(call_options = {}, options_block = nil)
1093
1268
  class_call_options = @options.merge(call_options || {})
@@ -1108,7 +1283,10 @@ module MarkdownExec
1108
1283
 
1109
1284
  [['Script', :saved_filespec],
1110
1285
  ['StdOut', :logged_stdout_filespec]].each do |label, name|
1111
- 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
1112
1290
  end
1113
1291
 
1114
1292
  oq.map do |label, value, level|
@@ -1139,7 +1317,9 @@ module MarkdownExec
1139
1317
  def prepare_blocks_menu(blocks_in_file, opts)
1140
1318
  # next if fcb.fetch(:disabled, false)
1141
1319
  # next unless fcb.fetch(:name, '').present?
1142
- 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
+
1143
1323
  fcb.merge!(
1144
1324
  name: fcb.dname,
1145
1325
  label: BlockLabel.make(
@@ -1165,7 +1345,7 @@ module MarkdownExec
1165
1345
  fcb.oname = fcb.dname = fcb.title || ''
1166
1346
  return unless fcb.body
1167
1347
 
1168
- set_fcb_title(fcb)
1348
+ update_title_from_body(fcb)
1169
1349
 
1170
1350
  if block &&
1171
1351
  selected_messages.include?(:blocks) &&
@@ -1183,6 +1363,13 @@ module MarkdownExec
1183
1363
  block.call(:line, fcb)
1184
1364
  end
1185
1365
 
1366
+ class MenuOptions
1367
+ YES = 1
1368
+ NO = 2
1369
+ SCRIPT_TO_CLIPBOARD = 3
1370
+ SAVE_SCRIPT = 4
1371
+ end
1372
+
1186
1373
  ##
1187
1374
  # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1188
1375
  # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
@@ -1200,44 +1387,40 @@ module MarkdownExec
1200
1387
  ##
1201
1388
  def prompt_for_user_approval(opts, required_lines)
1202
1389
  # Present a selection menu for user approval.
1203
- sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
1204
- menu.default 1
1205
- menu.choice opts[:prompt_yes], 1
1206
- menu.choice opts[:prompt_no], 2
1207
- menu.choice opts[:prompt_script_to_clipboard], 3
1208
- 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
1209
1399
  end
1210
1400
 
1211
- if sel == 3
1401
+ if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1212
1402
  copy_to_clipboard(required_lines)
1213
- elsif sel == 4
1403
+ elsif sel == MenuOptions::SAVE_SCRIPT
1214
1404
  save_to_file(opts, required_lines)
1215
1405
  end
1216
1406
 
1217
- sel == 1
1218
- end
1219
-
1220
- ## insert back option at head or tail
1221
- #
1222
- ## Adds a back option at the head or tail of a menu
1223
- #
1224
- def prompt_menu_add_back(items, label)
1225
- return items unless @options[:menu_with_back] && history_state_exist?
1226
-
1227
- state = history_state_partition(@options)
1228
- @hs_curr = state[:unit]
1229
- @hs_rest = state[:rest]
1230
- @options[:menu_back_at_top] ? [label] + items : items + [label]
1407
+ sel == MenuOptions::YES
1408
+ rescue TTY::Reader::InputInterrupt
1409
+ exit 1
1231
1410
  end
1232
1411
 
1233
- ## insert exit option at head or tail
1234
- #
1235
- def prompt_menu_add_exit(items, label)
1236
- if @options[:menu_exit_at_top]
1237
- (@options[:menu_with_exit] ? [label] : []) + items
1238
- else
1239
- 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]
1240
1420
  end
1421
+ sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1422
+ rescue TTY::Reader::InputInterrupt
1423
+ exit 1
1241
1424
  end
1242
1425
 
1243
1426
  # :reek:UtilityFunction ### temp
@@ -1256,7 +1439,9 @@ module MarkdownExec
1256
1439
  temp_blocks = []
1257
1440
 
1258
1441
  temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
1259
- 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
1260
1445
 
1261
1446
  if File.exist?(temp_blocks_file_path)
1262
1447
  temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
@@ -1265,6 +1450,24 @@ module MarkdownExec
1265
1450
  temp_blocks
1266
1451
  end
1267
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
+
1268
1471
  def run
1269
1472
  clear_required_file
1270
1473
  execute_block_with_error_handling(initialize_and_parse_cli_options)
@@ -1282,7 +1485,15 @@ module MarkdownExec
1282
1485
 
1283
1486
  saved_name_split filename
1284
1487
  @options[:save_executed_script] = false
1285
- 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, $!
1286
1497
  end
1287
1498
 
1288
1499
  def save_to_file(opts, required_lines)
@@ -1309,39 +1520,48 @@ module MarkdownExec
1309
1520
  # @param call_options [Hash] Initial options for the method.
1310
1521
  # @param options_block [Block] Block of options to be merged with call_options.
1311
1522
  # @return [Nil] Returns nil if no code block is selected or an error occurs.
1312
- def select_approve_and_execute_block(call_options, &options_block)
1313
- opts = optsmerge(call_options, options_block)
1314
- repeat_menu = true && !opts[:block_name].present?
1315
- load_file = !LOAD_FILE
1316
- 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
1317
1530
 
1318
1531
  loop do
1319
1532
  loop do
1320
- opts[:back] = false
1321
- blocks_in_file, blocks_menu, mdoc = load_file_and_prepare_menu(opts)
1322
-
1323
- unless opts[:block_name].present?
1324
- block_name, state = wait_for_user_selection(blocks_in_file, blocks_menu, default,
1325
- opts)
1326
- case state
1327
- when :exit
1328
- return nil
1329
- when :back
1330
- opts[:block_name] = block_name[:option]
1331
- opts[:back] = true
1332
- when :continue
1333
- opts[:block_name] = block_name
1334
- 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
1335
1553
  end
1336
1554
 
1337
- load_file, next_block_name = approve_and_execute_block(opts, mdoc)
1338
- default = load_file == LOAD_FILE ? 1 : opts[:block_name]
1339
- opts[:block_name] = next_block_name
1340
-
1341
- 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
1342
1560
  break unless repeat_menu
1343
1561
  end
1344
- break if load_file != LOAD_FILE
1562
+ break if load_file == LoadFile::Reuse
1563
+
1564
+ repeat_menu = next_block_name_from_command_line_arguments(base_opts)
1345
1565
  end
1346
1566
  rescue StandardError => err
1347
1567
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1371,21 +1591,24 @@ module MarkdownExec
1371
1591
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1372
1592
  def select_option_with_metadata(prompt_text, items, opts = {})
1373
1593
  selection = @prompt.select(prompt_text,
1374
- prompt_menu_add_exit(
1375
- prompt_menu_add_back(
1376
- items,
1377
- opts[:menu_option_back_name]
1378
- ),
1379
- opts[:menu_option_exit_name]
1380
- ),
1594
+ items,
1381
1595
  opts.merge(filter: true))
1382
- if selection == opts[:menu_option_back_name]
1383
- { option: selection, curr: @hs_curr, rest: @hs_rest, shell: BLOCK_TYPE_LINK }
1384
- elsif selection == opts[:menu_option_exit_name]
1385
- { option: selection }
1386
- else
1387
- { selected: selection }
1388
- 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
1389
1612
  end
1390
1613
 
1391
1614
  def select_recent_output
@@ -1417,21 +1640,13 @@ module MarkdownExec
1417
1640
 
1418
1641
  saved_name_split(filename)
1419
1642
 
1420
- select_approve_and_execute_block({
1421
- bash: true,
1643
+ select_approve_and_execute_block({ bash: true,
1422
1644
  save_executed_script: false,
1423
- struct: true
1424
- })
1645
+ struct: true })
1425
1646
  end
1426
1647
 
1427
- # set the title of an FCB object based on its body if it is nil or empty
1428
- def set_fcb_title(fcb)
1429
- return unless fcb.title.nil? || fcb.title.empty?
1430
-
1431
- fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
1432
- end
1433
-
1434
- def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
1648
+ def start_fenced_block(opts, line, headings,
1649
+ fenced_start_extended_regex)
1435
1650
  fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1436
1651
  rest = fcb_title_groups.fetch(:rest, '')
1437
1652
 
@@ -1464,7 +1679,11 @@ module MarkdownExec
1464
1679
  end
1465
1680
 
1466
1681
  def tty_prompt_without_disabled_symbol
1467
- TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
1682
+ TTY::Prompt.new(interrupt: lambda {
1683
+ puts;
1684
+ raise TTY::Reader::InputInterrupt
1685
+ },
1686
+ symbols: { cross: ' ' })
1468
1687
  end
1469
1688
 
1470
1689
  ##
@@ -1511,7 +1730,8 @@ module MarkdownExec
1511
1730
  #
1512
1731
  # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1513
1732
  ##
1514
- 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)
1515
1735
  if opts[:menu_blocks_with_headings]
1516
1736
  state[:headings] =
1517
1737
  update_document_headings(line, state[:headings], opts)
@@ -1519,7 +1739,8 @@ module MarkdownExec
1519
1739
 
1520
1740
  if line.match(state[:fenced_start_and_end_regex])
1521
1741
  if state[:in_fenced_block]
1522
- process_fenced_block(state[:fcb], opts, selected_messages, &block)
1742
+ process_fenced_block(state[:fcb], opts, selected_messages,
1743
+ &block)
1523
1744
  state[:in_fenced_block] = false
1524
1745
  else
1525
1746
  state[:fcb] =
@@ -1545,26 +1766,59 @@ module MarkdownExec
1545
1766
  @options
1546
1767
  end
1547
1768
 
1548
- ## 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
1549
1792
  #
1550
- 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)
1551
1795
  pt = opts[:prompt_select_block].to_s
1552
1796
  bm = prepare_blocks_menu(blocks_menu, opts)
1553
- 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
1554
1804
 
1555
- obj = select_option_with_metadata(pt, bm, opts.merge(
1556
- default: default,
1805
+ obj = select_option_with_metadata(pt, bm, o2.merge(
1557
1806
  per_page: opts[:select_page_height]
1558
1807
  ))
1559
- case obj.fetch(:option, nil)
1560
- when opts[:menu_option_exit_name]
1561
- [nil, :exit]
1562
- when opts[:menu_option_back_name]
1563
- [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]
1564
1814
  else
1565
- label_block = blocks_in_file.find { |fcb| fcb.dname == obj[:selected] }
1566
- [label_block.oname, :continue]
1815
+ [obj, MenuState::CONTINUE]
1567
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
1568
1822
  end
1569
1823
 
1570
1824
  # Handles the core logic for generating the command file's metadata and content.
@@ -1581,7 +1835,8 @@ module MarkdownExec
1581
1835
 
1582
1836
  @execute_script_filespec =
1583
1837
  @options[:saved_filespec] =
1584
- File.join opts[:saved_script_folder], opts[:saved_script_filename]
1838
+ File.join opts[:saved_script_folder],
1839
+ opts[:saved_script_filename]
1585
1840
 
1586
1841
  shebang = if @options[:shebang]&.present?
1587
1842
  "#{@options[:shebang]} #{@options[:shell]}\n"
@@ -1625,7 +1880,6 @@ module MarkdownExec
1625
1880
  )[:code]).join("\n")
1626
1881
 
1627
1882
  Dir::Tmpname.create(self.class.to_s) do |path|
1628
- pp path
1629
1883
  File.write(path, code_blocks)
1630
1884
  ENV['MDE_LINK_REQUIRED_FILE'] = path
1631
1885
  end
@@ -1645,7 +1899,7 @@ if $PROGRAM_NAME == __FILE__
1645
1899
 
1646
1900
  def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1647
1901
  pigeon = 'E'
1648
- obj = { pass_args: pigeon }
1902
+ obj = { s_pass_args: pigeon }
1649
1903
 
1650
1904
  c = MarkdownExec::MarkParse.new
1651
1905
 
@@ -1665,15 +1919,17 @@ if $PROGRAM_NAME == __FILE__
1665
1919
  end
1666
1920
 
1667
1921
  def test_set_fcb_title
1668
- # sample input and output data for testing set_fcb_title method
1922
+ # sample input and output data for testing update_title_from_body method
1669
1923
  input_output_data = [
1670
1924
  {
1671
1925
  input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1672
1926
  output: "puts 'Hello, world!'"
1673
1927
  },
1674
1928
  {
1675
- input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
1676
- 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"
1677
1933
  },
1678
1934
  {
1679
1935
  input: FCB.new(title: 'foo', body: %w[bar baz]),
@@ -1686,10 +1942,20 @@ if $PROGRAM_NAME == __FILE__
1686
1942
  input_output_data.each do |data|
1687
1943
  input = data[:input]
1688
1944
  output = data[:output]
1689
- @mark_parse.set_fcb_title(input)
1945
+ @mark_parse.update_title_from_body(input)
1690
1946
  assert_equal output, input.title
1691
1947
  end
1692
1948
  end
1693
1949
  end
1694
- end
1695
- 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