markdown_exec 1.4.1 → 1.6

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