markdown_exec 1.4 → 1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/markdown_exec.rb CHANGED
@@ -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