markdown_exec 2.2.0 → 2.4.0

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +16 -4
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +32 -8
  6. data/bats/bats.bats +33 -0
  7. data/bats/block-types.bats +56 -0
  8. data/bats/cli.bats +74 -0
  9. data/bats/fail.bats +11 -0
  10. data/bats/history.bats +34 -0
  11. data/bats/markup.bats +66 -0
  12. data/bats/mde.bats +29 -0
  13. data/bats/options.bats +92 -0
  14. data/bats/test_helper.bash +152 -0
  15. data/bin/tab_completion.sh +44 -20
  16. data/docs/dev/block-type-opts.md +10 -0
  17. data/docs/dev/block-type-port.md +24 -0
  18. data/docs/dev/block-type-vars.md +7 -0
  19. data/docs/dev/pass-through-arguments.md +8 -0
  20. data/docs/dev/specs-import.md +9 -0
  21. data/docs/dev/specs.md +83 -0
  22. data/docs/dev/text-decoration.md +7 -0
  23. data/examples/bash-blocks.md +4 -4
  24. data/examples/block-names.md +40 -5
  25. data/examples/import0.md +23 -0
  26. data/examples/import1.md +13 -0
  27. data/examples/link-blocks-vars.md +3 -3
  28. data/examples/opts-blocks-require.md +6 -6
  29. data/examples/table-markup.md +31 -0
  30. data/examples/text-markup.md +58 -0
  31. data/examples/vars-blocks.md +2 -2
  32. data/examples/wrap.md +87 -9
  33. data/lib/ansi_formatter.rb +12 -6
  34. data/lib/ansi_string.rb +153 -0
  35. data/lib/argument_processor.rb +160 -0
  36. data/lib/cached_nested_file_reader.rb +4 -2
  37. data/lib/ce_get_cost_and_usage.rb +4 -3
  38. data/lib/cli.rb +1 -1
  39. data/lib/colorize.rb +41 -0
  40. data/lib/constants.rb +17 -0
  41. data/lib/directory_searcher.rb +4 -2
  42. data/lib/doh.rb +190 -0
  43. data/lib/env.rb +1 -1
  44. data/lib/exceptions.rb +9 -6
  45. data/lib/fcb.rb +0 -199
  46. data/lib/filter.rb +18 -5
  47. data/lib/find_files.rb +8 -3
  48. data/lib/format_table.rb +406 -0
  49. data/lib/hash_delegator.rb +939 -611
  50. data/lib/hierarchy_string.rb +221 -0
  51. data/lib/input_sequencer.rb +19 -11
  52. data/lib/instance_method_wrapper.rb +2 -1
  53. data/lib/layered_hash.rb +143 -0
  54. data/lib/link_history.rb +22 -8
  55. data/lib/markdown_exec/version.rb +1 -1
  56. data/lib/markdown_exec.rb +420 -165
  57. data/lib/mdoc.rb +38 -38
  58. data/lib/menu.src.yml +832 -680
  59. data/lib/menu.yml +814 -689
  60. data/lib/namer.rb +6 -12
  61. data/lib/object_present.rb +1 -1
  62. data/lib/option_value.rb +7 -3
  63. data/lib/poly.rb +33 -14
  64. data/lib/resize_terminal.rb +60 -52
  65. data/lib/saved_assets.rb +45 -34
  66. data/lib/saved_files_matcher.rb +6 -3
  67. data/lib/streams_out.rb +7 -1
  68. data/lib/table_extractor.rb +166 -0
  69. data/lib/tap.rb +5 -6
  70. data/lib/text_analyzer.rb +236 -0
  71. metadata +28 -3
  72. data/lib/std_out_err_logger.rb +0 -119
@@ -17,6 +17,7 @@ require 'tmpdir'
17
17
  require 'tty-prompt'
18
18
  require 'yaml'
19
19
 
20
+ require_relative 'ansi_string'
20
21
  require_relative 'array'
21
22
  require_relative 'array_util'
22
23
  require_relative 'block_label'
@@ -27,16 +28,21 @@ require_relative 'directory_searcher'
27
28
  require_relative 'exceptions'
28
29
  require_relative 'fcb'
29
30
  require_relative 'filter'
31
+ require_relative 'format_table'
30
32
  require_relative 'fout'
31
33
  require_relative 'hash'
34
+ require_relative 'hierarchy_string'
32
35
  require_relative 'link_history'
33
36
  require_relative 'mdoc'
34
37
  require_relative 'namer'
35
38
  require_relative 'regexp'
36
39
  require_relative 'resize_terminal'
37
- require_relative 'std_out_err_logger'
38
40
  require_relative 'streams_out'
39
41
  require_relative 'string_util'
42
+ require_relative 'table_extractor'
43
+ require_relative 'text_analyzer'
44
+
45
+ require_relative 'argument_processor'
40
46
 
41
47
  $pd = false unless defined?($pd)
42
48
 
@@ -50,17 +56,20 @@ end
50
56
 
51
57
  module HashDelegatorSelf
52
58
  # Applies an ANSI color method to a string using a specified color key.
53
- # The method retrieves the color method from the provided hash. If the color key
54
- # is not present in the hash, it uses a default color method.
59
+ # The method retrieves the color method from the provided hash. If the
60
+ # color key is not present in the hash, it uses a default color method.
55
61
  # @param string [String] The string to be colored.
56
- # @param color_methods [Hash] A hash where keys are color names (String/Symbol) and values are color methods.
57
- # @param color_key [String, Symbol] The key representing the desired color method in the color_methods hash.
58
- # @param default_method [String] (optional) Default color method to use if color_key is not found in color_methods. Defaults to 'plain'.
62
+ # @param color_methods [Hash] A hash where keys are color names
63
+ # (String/Symbol) and values are color methods.
64
+ # @param color_key [String, Symbol] The key representing the desired
65
+ # color method in the color_methods hash.
66
+ # @param default_method [String] (optional) Default color method to
67
+ # use if color_key is not found in color_methods. Defaults to 'plain'.
59
68
  # @return [String] The colored string.
60
69
  def apply_color_from_hash(string, color_methods, color_key,
61
70
  default_method: 'plain')
62
71
  color_method = color_methods.fetch(color_key, default_method).to_sym
63
- string.to_s.send(color_method)
72
+ AnsiString.new(string.to_s).send(color_method)
64
73
  end
65
74
 
66
75
  # # Enhanced `apply_color_from_hash` method to support dynamic color transformations
@@ -82,15 +91,21 @@ module HashDelegatorSelf
82
91
  # colored_string = apply_color_from_hash(string, color_transformations, :red)
83
92
  # puts colored_string # This will print the string in red
84
93
 
85
- # Searches for the first element in a collection where the specified message sent to an element matches a given value.
86
- # This method is particularly useful for finding a specific hash-like object within an enumerable collection.
94
+ # Searches for the first element in a collection where the specified
95
+ # message sent to an element matches a given value.
96
+ # This method is particularly useful for finding a specific hash-like
97
+ # object within an enumerable collection.
87
98
  # If no match is found, it returns a specified default value.
88
99
  #
89
100
  # @param blocks [Enumerable] The collection of hash-like objects to search.
90
- # @param msg [Symbol, String] The message to send to each element of the collection.
91
- # @param value [Object] The value to match against the result of the message sent to each element.
92
- # @param default [Object, nil] The default value to return if no match is found (optional).
93
- # @return [Object, nil] The first matching element or the default value if no match is found.
101
+ # @param msg [Symbol, String] The message to send to each element of
102
+ # the collection.
103
+ # @param value [Object] The value to match against the result of the message
104
+ # sent to each element.
105
+ # @param default [Object, nil] The default value to return if no match is
106
+ # found (optional).
107
+ # @return [Object, nil] The first matching element or the default value if
108
+ # no match is found.
94
109
  def block_find(blocks, msg, value, default = nil)
95
110
  blocks.find { |item| item.send(msg) == value } || default
96
111
  end
@@ -108,11 +123,13 @@ module HashDelegatorSelf
108
123
  end
109
124
 
110
125
  # Creates a file at the specified path, writes the given content to it,
111
- # and sets file permissions if required. Handles any errors encountered during the process.
126
+ # and sets file permissions if required. Handles any errors encountered
127
+ # during the process.
112
128
  #
113
129
  # @param file_path [String] The path where the file will be created.
114
130
  # @param content [String] The content to write into the file.
115
- # @param chmod_value [Integer] The file permission value to set; skips if zero.
131
+ # @param chmod_value [Integer] The file permission value to set;
132
+ # skips if zero.
116
133
  def create_file_and_write_string_with_permissions(file_path, content,
117
134
  chmod_value)
118
135
  create_directory_for_file(file_path)
@@ -126,7 +143,8 @@ module HashDelegatorSelf
126
143
  # Dir::Tmpname.create(self.class.to_s) { |path| path }
127
144
  # end
128
145
 
129
- # Updates the title of an FCB object from its body content if the title is nil or empty.
146
+ # Updates the title of an FCB object from its body content if the title
147
+ # is nil or empty.
130
148
  def default_block_title_from_body(fcb)
131
149
  return unless fcb.title.nil? || fcb.title.empty?
132
150
 
@@ -172,7 +190,8 @@ module HashDelegatorSelf
172
190
 
173
191
  # Indents all lines in a given string with a specified indentation string.
174
192
  # @param body [String] A multi-line string to be indented.
175
- # @param indent [String] The string used for indentation (default is an empty string).
193
+ # @param indent [String] The string used for indentation
194
+ # (default is an empty string).
176
195
  # @return [String] A single string with each line indented as specified.
177
196
  def indent_all_lines(body, indent = nil)
178
197
  return body unless indent&.non_empty?
@@ -269,13 +288,51 @@ module HashDelegatorSelf
269
288
  File.chmod(chmod_value, file_path)
270
289
  end
271
290
 
291
+ # find tables in multiple lines and format horizontally
292
+ def tables_into_columns!(blocks_menu, delegate_object)
293
+ return unless delegate_object[:tables_into_columns]
294
+
295
+ lines = blocks_menu.map(&:oname)
296
+ text_tables = TableExtractor.extract_tables(lines)
297
+ return unless text_tables.count.positive?
298
+
299
+ text_tables.each do |match|
300
+ range = match[:start_index]..(match[:start_index] + match[:rows] - 1)
301
+ lines = blocks_menu[range].map(&:oname)
302
+ formatted = MarkdownTableFormatter.format_table(
303
+ lines,
304
+ match[:columns],
305
+ decorate: {
306
+ border: delegate_object[:table_border_color],
307
+ header_row: delegate_object[:table_header_row_color],
308
+ row: delegate_object[:table_row_color],
309
+ separator_line: delegate_object[:table_separator_line_color]
310
+ }
311
+ )
312
+
313
+ if formatted.count == range.size
314
+ # read indentation from first line
315
+ indent = blocks_menu[range.first].oname.split('|', 2).first
316
+
317
+ # replace text in each block
318
+ range.each.with_index do |block_ind, ind|
319
+ ### format oname to dname
320
+ blocks_menu[block_ind].dname = indent + formatted[ind]
321
+ end
322
+ else
323
+ warn [__LINE__, range, lines, formatted].inspect
324
+ raise 'Invalid result from MarkdownTableFormatter.format_table()'
325
+ end
326
+ end
327
+ end
328
+
272
329
  # Creates a TTY prompt with custom settings. Specifically, it disables the default 'cross' symbol and
273
330
  # defines a lambda function to handle interrupts.
274
331
  # @return [TTY::Prompt] A new TTY::Prompt instance with specified configurations.
275
332
  def tty_prompt_without_disabled_symbol
276
333
  TTY::Prompt.new(
277
334
  interrupt: lambda {
278
- puts
335
+ puts # next line in case not at start
279
336
  raise TTY::Reader::InputInterrupt
280
337
  },
281
338
  symbols: { cross: ' ' }
@@ -287,7 +344,7 @@ module HashDelegatorSelf
287
344
  # If the fcb has a body and meets certain conditions, it yields to the given block.
288
345
  #
289
346
  # @param fcb [Object] The fcb object whose attributes are to be updated.
290
- # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable.
347
+ # @param selected_types [Array<Symbol>] A list of message types to determine if yielding is applicable.
291
348
  # @param block [Block] An optional block to yield to if conditions are met.
292
349
  def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {},
293
350
  &block)
@@ -301,10 +358,10 @@ module HashDelegatorSelf
301
358
 
302
359
  # Yields a line as a new block if the selected message type includes :line.
303
360
  # @param [String] line The line to be processed.
304
- # @param [Array<Symbol>] selected_messages A list of message types to check.
361
+ # @param [Array<Symbol>] selected_types A list of message types to check.
305
362
  # @param [Proc] block The block to be called with the line data.
306
- def yield_line_if_selected(line, selected_messages, &block)
307
- return unless block && selected_messages.include?(:line)
363
+ def yield_line_if_selected(line, selected_types, &block)
364
+ return unless block && block_type_selected?(selected_types, :line)
308
365
 
309
366
  block.call(:line, MarkdownExec::FCB.new(body: [line]))
310
367
  end
@@ -478,10 +535,12 @@ module MarkdownExec
478
535
  end
479
536
 
480
537
  class HashDelegatorParent
481
- attr_accessor :most_recent_loaded_filename, :pass_args, :run_state
538
+ attr_accessor :most_recent_loaded_filename, :pass_args, :run_state,
539
+ :p_all_arguments, :p_options_parsed, :p_params, :p_rest
482
540
 
483
541
  extend HashDelegatorSelf
484
542
  include CompactionHelpers
543
+ include TextAnalyzer
485
544
 
486
545
  def initialize(delegate_object = {})
487
546
  @delegate_object = delegate_object
@@ -498,6 +557,11 @@ module MarkdownExec
498
557
 
499
558
  @process_mutex = Mutex.new
500
559
  @process_cv = ConditionVariable.new
560
+
561
+ @p_all_arguments = []
562
+ @p_options_parsed = []
563
+ @p_params = {}
564
+ @p_rest = []
501
565
  end
502
566
 
503
567
  # private
@@ -510,6 +574,26 @@ module MarkdownExec
510
574
  # @delegate_object[key] = value
511
575
  # end
512
576
 
577
+ ##
578
+ # Returns the absolute path of the given file path.
579
+ # If the provided path is already absolute, it returns it as is.
580
+ # Otherwise, it prefixes the path with the current working directory.
581
+ #
582
+ # @param file_path [String] The file path to process
583
+ # @return [String] The absolute path
584
+ #
585
+ # Example usage:
586
+ # absolute_path('/absolute/path/to/file.txt') # => '/absolute/path/to/file.txt'
587
+ # absolute_path('relative/path/to/file.txt') # => '/current/working/directory/relative/path/to/file.txt'
588
+ #
589
+ def absolute_path(file_path)
590
+ if File.absolute_path?(file_path)
591
+ file_path
592
+ else
593
+ File.join(Dir.getwd, file_path)
594
+ end
595
+ end
596
+
513
597
  # Modifies the provided menu blocks array by adding 'Back' and 'Exit' options,
514
598
  # along with initial and final dividers, based on the delegate object's configuration.
515
599
  #
@@ -530,7 +614,8 @@ module MarkdownExec
530
614
  add_exit_option(menu_blocks: menu_blocks)
531
615
  end
532
616
 
533
- add_dividers(menu_blocks: menu_blocks)
617
+ append_divider(menu_blocks: menu_blocks, position: :initial)
618
+ append_divider(menu_blocks: menu_blocks, position: :final)
534
619
  end
535
620
 
536
621
  private
@@ -539,11 +624,6 @@ module MarkdownExec
539
624
  append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::BACK)
540
625
  end
541
626
 
542
- def add_dividers(menu_blocks:)
543
- append_divider(menu_blocks: menu_blocks, position: :initial)
544
- append_divider(menu_blocks: menu_blocks, position: :final)
545
- end
546
-
547
627
  def add_exit_option(menu_blocks:)
548
628
  append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::EXIT)
549
629
  end
@@ -596,6 +676,7 @@ module MarkdownExec
596
676
  dname: HashDelegator.new(@delegate_object).string_send_color(
597
677
  formatted_name, :menu_chrome_color
598
678
  ),
679
+ nickname: formatted_name,
599
680
  oname: formatted_name
600
681
  )
601
682
 
@@ -668,6 +749,16 @@ module MarkdownExec
668
749
  end
669
750
  end
670
751
 
752
+ def apply_tree_decorations(text, color_method, decor_patterns)
753
+ tree = HierarchyString.new([{ text: text, color: color_method }])
754
+ decor_patterns.each do |pc|
755
+ analyzed_hierarchy = TextAnalyzer.analyze_hierarchy(tree.substrings, pc[:pattern],
756
+ color_method, pc[:color_method])
757
+ tree = HierarchyString.new(analyzed_hierarchy)
758
+ end
759
+ tree.decorate
760
+ end
761
+
671
762
  def assign_key_value_in_bash(key, value)
672
763
  if value =~ /["$\\`]/
673
764
  # requiring ShellWords to write into Bash scripts
@@ -685,6 +776,7 @@ module MarkdownExec
685
776
  # @return [Array<FCB>] An array of FCB objects representing the blocks.
686
777
  def blocks_from_nested_files
687
778
  register_console_attributes(@delegate_object)
779
+ @decor_patterns_from_delegate_object_for_block_create = collect_line_decor_patterns(@delegate_object)
688
780
 
689
781
  blocks = []
690
782
  iter_blocks_from_nested_files do |btype, fcb|
@@ -700,9 +792,7 @@ module MarkdownExec
700
792
  # if matched, the block returned has properties that it is from cli and not ui
701
793
  def block_state_for_name_from_cli(block_name)
702
794
  SelectedBlockMenuState.new(
703
- @dml_blocks_in_file.find do |item|
704
- block_name == item.pub_name
705
- end,
795
+ blocks_find_by_block_name(@dml_blocks_in_file, block_name),
706
796
  OpenStruct.new(
707
797
  block_name_from_cli: true,
708
798
  block_name_from_ui: false
@@ -711,6 +801,17 @@ module MarkdownExec
711
801
  )
712
802
  end
713
803
 
804
+ def blocks_find_by_block_name(blocks, block_name)
805
+ @dml_blocks_in_file.find do |item|
806
+ # 2024-08-04 match oname for long block names
807
+ # 2024-08-04 match nickname for long block names
808
+ block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
809
+ end || @dml_menu_blocks.find do |item|
810
+ # 2024-08-22 search in menu blocks to allow matching of automatic chrome with nickname
811
+ block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
812
+ end
813
+ end
814
+
714
815
  # private
715
816
 
716
817
  def calc_logged_stdout_filename(block_name:)
@@ -752,6 +853,23 @@ module MarkdownExec
752
853
  true
753
854
  end
754
855
 
856
+ def collect_line_decor_patterns(delegate_object)
857
+ extract_patterns = lambda do |key|
858
+ return [] unless delegate_object[key].present?
859
+
860
+ HashDelegator.safeval(delegate_object[key]).map do |pc|
861
+ {
862
+ color_method: pc[:color_method].to_sym,
863
+ pattern: Regexp.new(pc[:pattern])
864
+ }
865
+ end
866
+ end
867
+
868
+ %i[line_decor_pre line_decor_main line_decor_post].flat_map do |key|
869
+ extract_patterns.call(key)
870
+ end
871
+ end
872
+
755
873
  # Collects required code lines based on the selected block and the delegate object's configuration.
756
874
  # If the block type is VARS, it also sets environment variables based on the block's content.
757
875
  #
@@ -777,7 +895,7 @@ module MarkdownExec
777
895
  runtime_exception(:runtime_exception_error_level,
778
896
  'unmet_dependencies, flag: runtime_exception_error_level',
779
897
  required[:unmet_dependencies])
780
- elsif false ### use option 2024-08-02
898
+ elsif @delegate_object[:dump_dependencies]
781
899
  warn format_and_highlight_dependencies(dependencies,
782
900
  highlight: [@delegate_object[:block_name]])
783
901
  end
@@ -861,16 +979,20 @@ module MarkdownExec
861
979
  # @param selected [Hash] The selected item from the menu to be executed.
862
980
  # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one.
863
981
  def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:,
864
- link_state: nil)
865
- required_lines = collect_required_code_lines(mdoc: mdoc, selected: selected, link_state: link_state,
866
- block_source: block_source)
982
+ link_state:)
983
+ required_lines = collect_required_code_lines(
984
+ mdoc: mdoc, selected: selected,
985
+ link_state: link_state, block_source: block_source
986
+ )
867
987
  output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
868
988
  if output_or_approval
869
989
  display_required_code(required_lines: required_lines)
870
990
  end
871
991
  allow_execution = if @delegate_object[:user_must_approve]
872
- prompt_for_user_approval(required_lines: required_lines,
873
- selected: selected)
992
+ prompt_for_user_approval(
993
+ required_lines: required_lines,
994
+ selected: selected
995
+ )
874
996
  else
875
997
  true
876
998
  end
@@ -881,7 +1003,6 @@ module MarkdownExec
881
1003
  end
882
1004
 
883
1005
  link_state.block_name = nil
884
- LoadFileLinkState.new(LoadFile::REUSE, link_state)
885
1006
  end
886
1007
 
887
1008
  # Check if the expression contains wildcard characters
@@ -920,6 +1041,7 @@ module MarkdownExec
920
1041
  format_option:, color_method:,
921
1042
  case_conversion: nil,
922
1043
  center: nil,
1044
+ decor_patterns: [],
923
1045
  wrap: nil)
924
1046
  line_cap = match_data.named_captures.transform_keys(&:to_sym)
925
1047
 
@@ -970,11 +1092,14 @@ module MarkdownExec
970
1092
  # format expects :line to be text only
971
1093
  line_obj[:line] = line_obj[:text]
972
1094
  oname = format(format_option, line_obj)
1095
+
1096
+ decorated = apply_tree_decorations(oname, color_method, decor_patterns)
1097
+
973
1098
  line_obj[:line] = line_obj[:indent] + line_obj[:text]
974
1099
  blocks.push FCB.new(
975
1100
  chrome: true,
976
1101
  disabled: '',
977
- dname: line_obj[:indent] + oname.send(color_method),
1102
+ dname: line_obj[:indent] + decorated,
978
1103
  oname: line_obj[:text]
979
1104
  )
980
1105
  end
@@ -988,6 +1113,7 @@ module MarkdownExec
988
1113
  # @param opts [Hash] Options containing configuration for line processing.
989
1114
  # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
990
1115
  def create_and_add_chrome_blocks(blocks, fcb)
1116
+ # rubocop:disable Layout/LineLength
991
1117
  match_criteria = [
992
1118
  { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, center: true, case_conversion: :upcase, wrap: true },
993
1119
  { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, center: true, wrap: true },
@@ -996,6 +1122,7 @@ module MarkdownExec
996
1122
  { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true },
997
1123
  { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true }
998
1124
  ]
1125
+ # rubocop:enable Layout/LineLength
999
1126
  # rubocop:enable Style/UnlessElse
1000
1127
  match_criteria.each do |criteria|
1001
1128
  unless @delegate_object[criteria[:match]].present? &&
@@ -1008,6 +1135,7 @@ module MarkdownExec
1008
1135
  case_conversion: criteria[:case_conversion],
1009
1136
  center: criteria[:center],
1010
1137
  color_method: @delegate_object[criteria[:color]].to_sym,
1138
+ decor_patterns: @decor_patterns_from_delegate_object_for_block_create,
1011
1139
  format_option: @delegate_object[criteria[:format]],
1012
1140
  match_data: mbody,
1013
1141
  wrap: criteria[:wrap]
@@ -1092,349 +1220,39 @@ module MarkdownExec
1092
1220
  @delegate_object[:menu_divider_format].present? && @delegate_object[divider_key].present?
1093
1221
  end
1094
1222
 
1095
- def do_save_execution_output
1096
- return unless @delegate_object[:save_execution_output]
1097
- return if @run_state.in_own_window
1098
-
1099
- @run_state.files.write_execution_output_to_file(@delegate_object[:logged_stdout_filespec])
1100
- end
1223
+ def dml_menu_append_chrome_item(
1224
+ name, count, type, menu_state: MenuState::LOAD,
1225
+ always_create: true, always_enable: true
1226
+ )
1227
+ raise unless name.present?
1228
+ raise if @dml_menu_blocks.nil?
1101
1229
 
1102
- # Select and execute a code block from a Markdown document.
1103
- #
1104
- # This method allows the user to interactively select a code block from a
1105
- # Markdown document, obtain approval, and execute the chosen block of code.
1106
- #
1107
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1108
- def document_inpseq
1109
- @menu_base_options = @delegate_object
1110
- @dml_link_state = LinkState.new(
1111
- block_name: @delegate_object[:block_name],
1112
- document_filename: @delegate_object[:filename]
1113
- )
1114
- @run_state.source.block_name_from_cli = @dml_link_state.block_name.present?
1115
- @cli_block_name = @dml_link_state.block_name
1116
- @dml_now_using_cli = @run_state.source.block_name_from_cli
1117
- @dml_menu_default_dname = nil
1118
- @dml_block_state = SelectedBlockMenuState.new
1119
- @doc_saved_lines_files = []
1230
+ item = @dml_menu_blocks.find { |block| block.oname == name }
1120
1231
 
1121
- ## load file with code lines per options
1232
+ # create menu item when it is needed (count > 0)
1122
1233
  #
1123
- if @menu_base_options[:load_code].present?
1124
- @dml_link_state.inherited_lines =
1125
- @menu_base_options[:load_code].split(':').map do |path|
1126
- File.readlines(path, chomp: true)
1127
- end.flatten(1)
1128
-
1129
- inherited_block_names = []
1130
- inherited_dependencies = {}
1131
- selected = FCB.new(oname: 'load_code')
1132
- pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names,
1133
- code_lines, inherited_dependencies, selected)
1134
- end
1135
-
1136
- fdo = ->(option) {
1137
- name = format(@delegate_object[:menu_link_format],
1138
- HashDelegator.safeval(@delegate_object[option]))
1139
- OpenStruct.new(
1140
- dname: name,
1141
- oname: name,
1142
- name: name,
1143
- pub_name: name.pub_name
1144
- )
1145
- }
1146
- item_back = fdo.call(:menu_option_back_name)
1147
- item_edit = fdo.call(:menu_option_edit_name)
1148
- item_history = fdo.call(:menu_option_history_name)
1149
- item_load = fdo.call(:menu_option_load_name)
1150
- item_save = fdo.call(:menu_option_save_name)
1151
- item_shell = fdo.call(:menu_option_shell_name)
1152
- item_view = fdo.call(:menu_option_view_name)
1153
-
1154
- @run_state.batch_random = Random.new.rand
1155
- @run_state.batch_index = 0
1156
-
1157
- @run_state.files = StreamsOut.new
1158
-
1159
- InputSequencer.new(
1160
- @delegate_object[:filename],
1161
- @delegate_object[:input_cli_rest]
1162
- ).run do |msg, data|
1163
- # &bt msg
1164
- case msg
1165
- when :parse_document # once for each menu
1166
- # puts "@ - parse document #{data}"
1167
- inpseq_parse_document(data)
1168
-
1169
- if @delegate_object[:menu_for_history]
1170
- history_files(@dml_link_state).tap do |files|
1171
- if files.count.positive?
1172
- menu_enable_option(item_history.oname, files.count, 'files',
1173
- menu_state: MenuState::HISTORY)
1174
- end
1175
- end
1176
- end
1177
-
1178
- if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
1179
-
1180
- sf = document_name_in_glob_as_file_name(
1181
- @dml_link_state.document_filename,
1182
- @delegate_object[:document_saved_lines_glob]
1183
- )
1184
- files = sf ? Dir.glob(sf) : []
1185
- @doc_saved_lines_files = files.count.positive? ? files : []
1186
-
1187
- lines_count = @dml_link_state.inherited_lines_count
1188
-
1189
- # add menu items (glob, load, save) and enable selectively
1190
- if files.count.positive? || lines_count.positive?
1191
- menu_add_disabled_option(sf)
1192
- end
1193
- if files.count.positive?
1194
- menu_enable_option(item_load.dname, files.count, 'files',
1195
- menu_state: MenuState::LOAD)
1196
- end
1197
- if lines_count.positive?
1198
- menu_enable_option(item_edit.dname, lines_count, 'lines',
1199
- menu_state: MenuState::EDIT)
1200
- end
1201
- if lines_count.positive?
1202
- menu_enable_option(item_save.dname, 1, '',
1203
- menu_state: MenuState::SAVE)
1204
- end
1205
- if lines_count.positive?
1206
- menu_enable_option(item_view.dname, 1, '',
1207
- menu_state: MenuState::VIEW)
1208
- end
1209
- if @delegate_object[:menu_with_shell]
1210
- menu_enable_option(item_shell.dname, 1, '',
1211
- menu_state: MenuState::SHELL)
1212
- end
1213
-
1214
- # # reflect new menu items
1215
- # @dml_mdoc = MDoc.new(@dml_menu_blocks)
1216
- end
1217
-
1218
- when :display_menu
1219
- # warn "@ - display menu:"
1220
- # ii_display_menu
1221
- @dml_block_state = SelectedBlockMenuState.new
1222
- @delegate_object[:block_name] = nil
1223
-
1224
- when :user_choice
1225
- if @dml_link_state.block_name.present?
1226
- # @prior_block_was_link = true
1227
- @dml_block_state.block = @dml_blocks_in_file.find do |item|
1228
- item.pub_name == @dml_link_state.block_name || item.oname == @dml_link_state.block_name
1229
- end
1230
- @dml_link_state.block_name = nil
1231
- else
1232
- # puts "? - Select a block to execute (or type #{$texit} to exit):"
1233
- break if inpseq_user_choice == :break # into @dml_block_state
1234
- break if @dml_block_state.block.nil? # no block matched
1235
- end
1236
- # puts "! - Executing block: #{data}"
1237
- @dml_block_state.block&.pub_name
1238
-
1239
- when :execute_block
1240
- case (block_name = data)
1241
- when item_back.pub_name
1242
- debounce_reset
1243
- @menu_user_clicked_back_link = true
1244
- load_file_link_state = pop_link_history_and_trigger_load
1245
- @dml_link_state = load_file_link_state.link_state
1246
-
1247
- InputSequencer.merge_link_state(
1248
- @dml_link_state,
1249
- InputSequencer.next_link_state(
1250
- block_name: @dml_link_state.block_name,
1251
- document_filename: @dml_link_state.document_filename,
1252
- prior_block_was_link: true
1253
- )
1254
- )
1255
-
1256
- when item_edit.pub_name
1257
- debounce_reset
1258
- edited = edit_text(@dml_link_state.inherited_lines_block)
1259
- @dml_link_state.inherited_lines = edited.split("\n") if edited
1260
-
1261
- return :break if pause_user_exit
1262
-
1263
- InputSequencer.next_link_state(prior_block_was_link: true)
1264
-
1265
- when item_history.pub_name
1266
- debounce_reset
1267
- files = history_files(@dml_link_state)
1268
- files_table_rows = files.map do |file|
1269
- if Regexp.new(@delegate_object[:saved_asset_match]) =~ file
1270
- begin
1271
- OpenStruct.new(
1272
- file: file,
1273
- row: format(
1274
- @delegate_object[:saved_history_format],
1275
- # create with default '*' so unknown parameters are given a wildcard
1276
- $~.names.each_with_object(Hash.new('*')) do |name, hash|
1277
- hash[name.to_sym] = $~[name]
1278
- end
1279
- )
1280
- )
1281
- rescue KeyError
1282
- # pp $!, $@
1283
- warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
1284
- error_handler('saved_history_format')
1285
- break
1286
- end
1287
- else
1288
- warn "Cannot parse name: #{file}"
1289
- next
1290
- end
1291
- end&.compact
1292
-
1293
- return :break unless files_table_rows
1294
-
1295
- # repeat select+display until user exits
1296
- row_attrib = :row
1297
- loop do
1298
- # menu with Back and Facet options at top
1299
- case (name = prompt_select_code_filename(
1300
- [@delegate_object[:prompt_filespec_back],
1301
- @delegate_object[:prompt_filespec_facet]] +
1302
- files_table_rows.map(&row_attrib),
1303
- string: @delegate_object[:prompt_select_history_file],
1304
- color_sym: :prompt_color_after_script_execution
1305
- ))
1306
- when @delegate_object[:prompt_filespec_back]
1307
- break
1308
- when @delegate_object[:prompt_filespec_facet]
1309
- row_attrib = row_attrib == :row ? :file : :row
1310
- else
1311
- file = files_table_rows.select { |ftr| ftr.row == name }&.first
1312
- info = file_info(file.file)
1313
- warn "#{file.file} - #{info[:lines]} lines / #{info[:size]} bytes"
1314
- warn(File.readlines(file.file,
1315
- chomp: false).map.with_index do |line, ind|
1316
- format(' %s. %s', format('% 4d', ind + 1).violet, line)
1317
- end)
1318
- end
1319
- end
1320
-
1321
- return :break if pause_user_exit
1322
-
1323
- InputSequencer.next_link_state(prior_block_was_link: true)
1324
-
1325
- when item_load.pub_name
1326
- debounce_reset
1327
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
1328
- @delegate_object[:document_saved_lines_glob])
1329
- load_filespec = load_filespec_from_expression(sf)
1330
- if load_filespec
1331
- @dml_link_state.inherited_lines_append(
1332
- File.readlines(load_filespec, chomp: true)
1333
- )
1334
- end
1335
-
1336
- return :break if pause_user_exit
1337
-
1338
- InputSequencer.next_link_state(prior_block_was_link: true)
1339
-
1340
- when item_save.pub_name
1341
- debounce_reset
1342
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
1343
- @delegate_object[:document_saved_lines_glob])
1344
- save_filespec = save_filespec_from_expression(sf)
1345
- if save_filespec && !write_file_with_directory_creation(
1346
- save_filespec,
1347
- HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
1348
- )
1349
- return :break
1350
-
1351
- end
1352
-
1353
- InputSequencer.next_link_state(prior_block_was_link: true)
1354
-
1355
- when item_shell.pub_name
1356
- debounce_reset
1357
- loop do
1358
- command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen)
1359
- break if !command.present? || command == 'exit'
1360
-
1361
- exit_status = execute_command_with_streams(
1362
- [@delegate_object[:shell], '-c', command]
1363
- )
1364
- case exit_status
1365
- when 0
1366
- warn "#{'OK'.green} #{exit_status}"
1367
- else
1368
- warn "#{'ERR'.bred} #{exit_status}"
1369
- end
1370
- end
1371
-
1372
- return :break if pause_user_exit
1373
-
1374
- InputSequencer.next_link_state(prior_block_was_link: true)
1375
-
1376
- when item_view.pub_name
1377
- debounce_reset
1378
- warn @dml_link_state.inherited_lines_block
1379
-
1380
- return :break if pause_user_exit
1381
-
1382
- InputSequencer.next_link_state(prior_block_was_link: true)
1383
-
1384
- else
1385
- @dml_block_state = block_state_for_name_from_cli(block_name)
1386
- if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS
1387
- debounce_reset
1388
- link_state = LinkState.new
1389
- options_state = read_show_options_and_trigger_reuse(
1390
- link_state: link_state,
1391
- mdoc: @dml_mdoc,
1392
- selected: @dml_block_state.block
1393
- )
1394
-
1395
- update_menu_base(options_state.options)
1396
- options_state.load_file_link_state.link_state
1397
- else
1398
- inpseq_execute_block(block_name)
1234
+ if item.nil? && (always_create || count.positive?)
1235
+ item = append_chrome_block(menu_blocks: @dml_menu_blocks,
1236
+ menu_state: menu_state)
1237
+ end
1399
1238
 
1400
- if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli,
1401
- selected: @dml_block_state.block)
1402
- return :break
1403
- end
1239
+ # update item if it exists
1240
+ #
1241
+ return unless item
1404
1242
 
1405
- ## order of block name processing: link block, cli, from user
1406
- #
1407
- @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break =
1408
- HashDelegator.next_link_state(
1409
- block_name: @dml_link_state.block_name,
1410
- block_name_from_cli: @dml_now_using_cli,
1411
- block_state: @dml_block_state,
1412
- was_using_cli: @dml_now_using_cli
1413
- )
1414
-
1415
- if !@dml_block_state.source.block_name_from_ui && cli_break
1416
- # &bsp '!block_name_from_ui + cli_break -> break'
1417
- return :break
1418
- end
1243
+ item.dname = type.present? ? "#{name} (#{count} #{type})" : name
1244
+ if always_enable || count.positive?
1245
+ item.delete(:disabled)
1246
+ else
1247
+ item[:disabled] = ''
1248
+ end
1249
+ end
1419
1250
 
1420
- InputSequencer.next_link_state(
1421
- block_name: @dml_link_state.block_name,
1422
- prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH
1423
- )
1424
- end
1425
- end
1251
+ def do_save_execution_output
1252
+ return unless @delegate_object[:save_execution_output]
1253
+ return if @run_state.in_own_window
1426
1254
 
1427
- when :exit?
1428
- data == $texit
1429
- when :stay?
1430
- data == $stay
1431
- else
1432
- raise "Invalid message: #{msg}"
1433
- end
1434
- end
1435
- rescue StandardError
1436
- HashDelegator.error_handler('document_inpseq',
1437
- { abort: true })
1255
+ @run_state.files.write_execution_output_to_file(@delegate_object[:logged_stdout_filespec])
1438
1256
  end
1439
1257
 
1440
1258
  # remove leading "./"
@@ -1449,9 +1267,9 @@ module MarkdownExec
1449
1267
  '_') })
1450
1268
  end
1451
1269
 
1452
- def dump_and_warn_block_state(selected:)
1270
+ def dump_and_warn_block_state(name:, selected:)
1453
1271
  if selected.nil?
1454
- Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
1272
+ Exceptions.warn_format("Block not found -- name: #{name}",
1455
1273
  { abort: true })
1456
1274
  end
1457
1275
 
@@ -1548,8 +1366,9 @@ module MarkdownExec
1548
1366
  result_text
1549
1367
  end
1550
1368
 
1551
- def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
1552
- lfls = execute_shell_type(
1369
+ def execute_block_for_state_and_name(selected:, mdoc:, link_state:,
1370
+ block_source: {})
1371
+ lfls = execute_block_by_type_for_lfls(
1553
1372
  selected: selected,
1554
1373
  mdoc: mdoc,
1555
1374
  link_state: link_state,
@@ -1558,8 +1377,26 @@ module MarkdownExec
1558
1377
 
1559
1378
  # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1560
1379
  [lfls.link_state,
1561
- lfls.load_file == LoadFile::LOAD ? nil : selected.dname]
1562
- #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1380
+ lfls.load_file == LoadFile::LOAD ? nil : selected.dname,
1381
+ # 2024-08-22 true to quit
1382
+ lfls.load_file == LoadFile::EXIT]
1383
+ end
1384
+
1385
+ def execute_block_in_state(block_name)
1386
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1387
+ dump_and_warn_block_state(name: block_name,
1388
+ selected: @dml_block_state.block)
1389
+ @dml_link_state, @dml_menu_default_dname, quit =
1390
+ execute_block_for_state_and_name(
1391
+ selected: @dml_block_state.block,
1392
+ mdoc: @dml_mdoc,
1393
+ link_state: @dml_link_state,
1394
+ block_source: {
1395
+ document_filename: @delegate_object[:filename],
1396
+ time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1397
+ }
1398
+ )
1399
+ :break if quit
1563
1400
  end
1564
1401
 
1565
1402
  # Executes a given command and processes its input, output, and error streams.
@@ -1612,6 +1449,82 @@ module MarkdownExec
1612
1449
  exit_status
1613
1450
  end
1614
1451
 
1452
+ def execute_history_select(
1453
+ files_table_rows,
1454
+ exit_prompt: @delegate_object[:prompt_filespec_back],
1455
+ pause_refresh: false,
1456
+ stream:
1457
+ )
1458
+ # repeat select+display until user exits
1459
+
1460
+ pause_now = false
1461
+ row_attrib = :row
1462
+ loop do
1463
+ if pause_now
1464
+ break if prompt_select_continue == MenuState::EXIT
1465
+ end
1466
+
1467
+ # menu with Back and Facet options at top
1468
+ case (name = prompt_select_code_filename(
1469
+ [exit_prompt,
1470
+ @delegate_object[:prompt_filespec_facet]] +
1471
+ files_table_rows.map(&row_attrib),
1472
+ string: @delegate_object[:prompt_select_history_file],
1473
+ color_sym: :prompt_color_after_script_execution
1474
+ ))
1475
+ when exit_prompt
1476
+ break
1477
+ when @delegate_object[:prompt_filespec_facet]
1478
+ row_attrib = row_attrib == :row ? :file : :row
1479
+ pause_now = false
1480
+ else
1481
+ file = files_table_rows.select { |ftr| ftr.row == name }&.first
1482
+ info = file_info(file.file)
1483
+ stream.puts "#{file.file} - #{info[:lines]} lines / " \
1484
+ "#{info[:size]} bytes"
1485
+ stream.puts(
1486
+ File.readlines(file.file,
1487
+ chomp: false).map.with_index do |line, ind|
1488
+ format(' %s. %s',
1489
+ AnsiString.new(format('% 4d', ind + 1)).send(:violet), line)
1490
+ end
1491
+ )
1492
+ pause_now = pause_refresh
1493
+ end
1494
+ end
1495
+ end
1496
+
1497
+ def execute_inherited_save
1498
+ save_filespec = save_filespec_from_expression(
1499
+ document_name_in_glob_as_file_name(
1500
+ @dml_link_state.document_filename,
1501
+ @delegate_object[:document_saved_lines_glob]
1502
+ )
1503
+ )
1504
+ if save_filespec && !write_file_with_directory_creation(
1505
+ save_filespec,
1506
+ HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
1507
+ )
1508
+ :break
1509
+ end
1510
+ end
1511
+
1512
+ def execute_navigate_back
1513
+ @menu_user_clicked_back_link = true
1514
+
1515
+ keep_code = @dml_link_state.keep_code
1516
+ inherited_lines = keep_code ? @dml_link_state.inherited_lines_block : nil
1517
+
1518
+ @dml_link_state = pop_link_history_new_state
1519
+
1520
+ {
1521
+ block_name: @dml_link_state.block_name,
1522
+ document_filename: @dml_link_state.document_filename,
1523
+ inherited_lines: inherited_lines,
1524
+ keep_code: keep_code
1525
+ }
1526
+ end
1527
+
1615
1528
  # Executes a block of code that has been approved for execution.
1616
1529
  # It sets the script block name, writes command files if required, and handles the execution
1617
1530
  # including output formatting and summarization.
@@ -1639,8 +1552,8 @@ module MarkdownExec
1639
1552
  # @param opts [Hash] Options hash containing configuration settings.
1640
1553
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
1641
1554
  #
1642
- def execute_shell_type(selected:, mdoc:, block_source:,
1643
- link_state: LinkState.new)
1555
+ def execute_block_by_type_for_lfls(selected:, mdoc:, block_source:,
1556
+ link_state: LinkState.new)
1644
1557
  if selected.shell == BlockType::LINK
1645
1558
  debounce_reset
1646
1559
  push_link_history_and_trigger_load(link_block_body: selected.body,
@@ -1649,9 +1562,18 @@ module MarkdownExec
1649
1562
  link_state: link_state,
1650
1563
  block_source: block_source)
1651
1564
 
1565
+ # from CLI
1566
+ elsif selected.nickname == @delegate_object[:menu_option_exit_name][:line]
1567
+ debounce_reset
1568
+ LoadFileLinkState.new(LoadFile::EXIT, link_state)
1569
+
1652
1570
  elsif @menu_user_clicked_back_link
1653
1571
  debounce_reset
1654
- pop_link_history_and_trigger_load
1572
+ # pop_link_history_new_state
1573
+ LoadFileLinkState.new(
1574
+ LoadFile::LOAD,
1575
+ pop_link_history_new_state
1576
+ )
1655
1577
 
1656
1578
  elsif selected.shell == BlockType::OPTS
1657
1579
  debounce_reset
@@ -1687,6 +1609,7 @@ module MarkdownExec
1687
1609
  selected: selected,
1688
1610
  link_state: link_state,
1689
1611
  block_source: block_source)
1612
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
1690
1613
 
1691
1614
  else
1692
1615
  LoadFileLinkState.new(LoadFile::REUSE, link_state)
@@ -1769,6 +1692,27 @@ module MarkdownExec
1769
1692
  expr.include?('%{') ? format_expression(expr) : expr
1770
1693
  end
1771
1694
 
1695
+ def fout_execution_report
1696
+ @fout.fout fetch_color(data_sym: :execution_report_preview_head,
1697
+ color_sym: :execution_report_preview_frame_color)
1698
+ [
1699
+ ['Block', @run_state.script_block_name],
1700
+ ['Command', ([MarkdownExec::BIN_NAME, @delegate_object[:filename]] +
1701
+ (@run_state.link_history.map { |item|
1702
+ item[:block_name]
1703
+ }) +
1704
+ [@run_state.script_block_name]).join(' ')],
1705
+ ['Script', @run_state.saved_filespec],
1706
+ ['StdOut', @delegate_object[:logged_stdout_filespec]]
1707
+ ].each do |label, value|
1708
+ next unless value
1709
+
1710
+ output_labeled_value(label, value, DISPLAY_LEVEL_ADMIN)
1711
+ end
1712
+ @fout.fout fetch_color(data_sym: :execution_report_preview_tail,
1713
+ color_sym: :execution_report_preview_frame_color)
1714
+ end
1715
+
1772
1716
  def generate_temp_filename(ext = '.sh')
1773
1717
  filename = begin
1774
1718
  Dir::Tmpname.make_tmpname(['x', ext], nil)
@@ -1886,45 +1830,6 @@ module MarkdownExec
1886
1830
  }
1887
1831
  end
1888
1832
 
1889
- def inpseq_execute_block(block_name)
1890
- @dml_block_state = block_state_for_name_from_cli(block_name)
1891
- dump_and_warn_block_state(selected: @dml_block_state.block)
1892
- @dml_link_state, @dml_menu_default_dname =
1893
- exec_bash_next_state(
1894
- selected: @dml_block_state.block,
1895
- mdoc: @dml_mdoc,
1896
- link_state: @dml_link_state,
1897
- block_source: {
1898
- document_filename: @delegate_object[:filename],
1899
- time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1900
- }
1901
- )
1902
- end
1903
-
1904
- def inpseq_parse_document(_document_filename)
1905
- @run_state.batch_index += 1
1906
- @run_state.in_own_window = false
1907
-
1908
- # &bsp 'loop', block_name_from_cli, @cli_block_name
1909
- @run_state.source.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
1910
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.source.block_name_from_cli,
1911
- now_using_cli: @dml_now_using_cli,
1912
- link_state: @dml_link_state)
1913
- end
1914
-
1915
- def inpseq_user_choice
1916
- @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1917
- menu_blocks: @dml_menu_blocks,
1918
- default: @dml_menu_default_dname)
1919
- # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
1920
- if !@dml_block_state
1921
- HashDelegator.error_handler('block_state missing', { abort: true })
1922
- elsif @dml_block_state.state == MenuState::EXIT
1923
- # &bsp 'load_cli_or_user_selected_block -> break'
1924
- :break
1925
- end
1926
- end
1927
-
1928
1833
  # Iterates through blocks in a file, applying the provided block to each line.
1929
1834
  # The iteration only occurs if the file exists.
1930
1835
  # @yield [Symbol] :filter Yields to obtain selected messages for processing.
@@ -1932,11 +1837,11 @@ module MarkdownExec
1932
1837
  return unless check_file_existence(@delegate_object[:filename])
1933
1838
 
1934
1839
  state = initial_state
1935
- selected_messages = yield :filter
1840
+ selected_types = yield :filter
1936
1841
  cfile.readlines(@delegate_object[:filename],
1937
1842
  import_paths: @delegate_object[:import_paths]&.split(':')).each do |nested_line|
1938
1843
  if nested_line
1939
- update_line_and_block_state(nested_line, state, selected_messages,
1844
+ update_line_and_block_state(nested_line, state, selected_types,
1940
1845
  &block)
1941
1846
  end
1942
1847
  end
@@ -1984,8 +1889,9 @@ module MarkdownExec
1984
1889
  label_format_above = @delegate_object[:shell_code_label_format_above]
1985
1890
  label_format_below = @delegate_object[:shell_code_label_format_below]
1986
1891
 
1987
- [label_format_above && format(label_format_above,
1988
- block_source.merge({ block_name: selected.pub_name }))] +
1892
+ [label_format_above.present? &&
1893
+ format(label_format_above,
1894
+ block_source.merge({ block_name: selected.pub_name }))] +
1989
1895
  output_lines.map do |line|
1990
1896
  re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
1991
1897
  next unless re =~ line
@@ -1994,14 +1900,17 @@ module MarkdownExec
1994
1900
  link_block_data.fetch('format',
1995
1901
  '%{line}'))
1996
1902
  end.compact +
1997
- [label_format_below && format(label_format_below,
1998
- block_source.merge({ block_name: selected.pub_name }))]
1903
+ [label_format_below.present? &&
1904
+ format(label_format_below,
1905
+ block_source.merge({ block_name: selected.pub_name }))]
1999
1906
  end
2000
1907
 
2001
1908
  def link_history_push_and_next(
2002
1909
  curr_block_name:, curr_document_filename:,
2003
1910
  inherited_block_names:, inherited_dependencies:, inherited_lines:,
1911
+ keep_code:,
2004
1912
  next_block_name:, next_document_filename:,
1913
+ next_keep_code:,
2005
1914
  next_load_file:
2006
1915
  )
2007
1916
  @link_history.push(
@@ -2010,7 +1919,8 @@ module MarkdownExec
2010
1919
  document_filename: curr_document_filename,
2011
1920
  inherited_block_names: inherited_block_names,
2012
1921
  inherited_dependencies: inherited_dependencies,
2013
- inherited_lines: inherited_lines
1922
+ inherited_lines: inherited_lines,
1923
+ keep_code: keep_code
2014
1924
  )
2015
1925
  )
2016
1926
  LoadFileLinkState.new(
@@ -2020,7 +1930,8 @@ module MarkdownExec
2020
1930
  document_filename: next_document_filename,
2021
1931
  inherited_block_names: inherited_block_names,
2022
1932
  inherited_dependencies: inherited_dependencies,
2023
- inherited_lines: inherited_lines
1933
+ inherited_lines: inherited_lines,
1934
+ keep_code: next_keep_code
2024
1935
  )
2025
1936
  )
2026
1937
  end
@@ -2076,8 +1987,6 @@ module MarkdownExec
2076
1987
  end
2077
1988
 
2078
1989
  SelectedBlockMenuState.new(block, source, state)
2079
- rescue StandardError
2080
- HashDelegator.error_handler('load_cli_or_user_selected_block')
2081
1990
  end
2082
1991
 
2083
1992
  # format + glob + select for file in load block
@@ -2118,6 +2027,23 @@ module MarkdownExec
2118
2027
  end
2119
2028
  end
2120
2029
 
2030
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:,
2031
+ link_state:)
2032
+ if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2033
+ # &bsp 'pause cli control, allow user to select block'
2034
+ block_name_from_cli = false
2035
+ now_using_cli = false
2036
+ @menu_base_options[:block_name] =
2037
+ @delegate_object[:block_name] = \
2038
+ link_state.block_name =
2039
+ @cli_block_name = nil
2040
+ end
2041
+
2042
+ @delegate_object = @menu_base_options.dup
2043
+ @menu_user_clicked_back_link = false
2044
+ [block_name_from_cli, now_using_cli]
2045
+ end
2046
+
2121
2047
  def mdoc_and_blocks_from_nested_files
2122
2048
  menu_blocks = blocks_from_nested_files
2123
2049
  mdoc = MDoc.new(menu_blocks) do |nopts|
@@ -2141,6 +2067,7 @@ module MarkdownExec
2141
2067
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
2142
2068
  ### compress empty lines
2143
2069
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
2070
+ HashDelegator.tables_into_columns!(menu_blocks, @delegate_object)
2144
2071
  [all_blocks, menu_blocks, mdoc] # &br
2145
2072
  end
2146
2073
 
@@ -2197,48 +2124,6 @@ module MarkdownExec
2197
2124
  end
2198
2125
  end
2199
2126
 
2200
- def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
2201
- raise unless name.present?
2202
- raise if @dml_menu_blocks.nil?
2203
-
2204
- item = @dml_menu_blocks.find { |block| block.oname == name }
2205
-
2206
- # create menu item when it is needed (count > 0)
2207
- #
2208
- if item.nil? && count.positive?
2209
- item = append_chrome_block(menu_blocks: @dml_menu_blocks,
2210
- menu_state: menu_state)
2211
- end
2212
-
2213
- # update item if it exists
2214
- #
2215
- return unless item
2216
-
2217
- item.dname = type.present? ? "#{name} (#{count} #{type})" : name
2218
- if count.positive?
2219
- item.delete(:disabled)
2220
- else
2221
- item[:disabled] = ''
2222
- end
2223
- end
2224
-
2225
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:,
2226
- link_state:)
2227
- if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2228
- # &bsp 'pause cli control, allow user to select block'
2229
- block_name_from_cli = false
2230
- now_using_cli = false
2231
- @menu_base_options[:block_name] =
2232
- @delegate_object[:block_name] = \
2233
- link_state.block_name =
2234
- @cli_block_name = nil
2235
- end
2236
-
2237
- @delegate_object = @menu_base_options.dup
2238
- @menu_user_clicked_back_link = false
2239
- [block_name_from_cli, now_using_cli]
2240
- end
2241
-
2242
2127
  # If a method is missing, treat it as a key for the @delegate_object.
2243
2128
  def method_missing(method_name, *args, &block)
2244
2129
  if @delegate_object.respond_to?(method_name)
@@ -2266,8 +2151,10 @@ module MarkdownExec
2266
2151
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2267
2152
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2268
2153
  inherited_lines: HashDelegator.code_merge(code_lines),
2154
+ keep_code: link_state&.keep_code,
2269
2155
  next_block_name: '',
2270
2156
  next_document_filename: @delegate_object[:filename],
2157
+ next_keep_code: false,
2271
2158
  next_load_file: LoadFile::REUSE
2272
2159
  )
2273
2160
  end
@@ -2278,27 +2165,6 @@ module MarkdownExec
2278
2165
  @fout.fout formatted_string
2279
2166
  end
2280
2167
 
2281
- def fout_execution_report
2282
- @fout.fout fetch_color(data_sym: :execution_report_preview_head,
2283
- color_sym: :execution_report_preview_frame_color)
2284
- [
2285
- ['Block', @run_state.script_block_name],
2286
- ['Command', ([MarkdownExec::BIN_NAME, @delegate_object[:filename]] +
2287
- (@run_state.link_history.map { |item|
2288
- item[:block_name]
2289
- }) +
2290
- [@run_state.script_block_name]).join(' ')],
2291
- ['Script', @run_state.saved_filespec],
2292
- ['StdOut', @delegate_object[:logged_stdout_filespec]]
2293
- ].each do |label, value|
2294
- next unless value
2295
-
2296
- output_labeled_value(label, value, DISPLAY_LEVEL_ADMIN)
2297
- end
2298
- @fout.fout fetch_color(data_sym: :execution_report_preview_tail,
2299
- color_sym: :execution_report_preview_frame_color)
2300
- end
2301
-
2302
2168
  def output_execution_summary
2303
2169
  return unless @delegate_object[:output_execution_summary]
2304
2170
 
@@ -2358,8 +2224,10 @@ module MarkdownExec
2358
2224
  (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2359
2225
  inherited_lines:
2360
2226
  HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2227
+ keep_code: link_state&.keep_code,
2361
2228
  next_block_name: next_block_name,
2362
2229
  next_document_filename: @delegate_object[:filename], # not next_document_filename
2230
+ next_keep_code: false,
2363
2231
  next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
2364
2232
  )
2365
2233
  # LoadFileLinkState.new(LoadFile::REUSE, link_state)
@@ -2367,20 +2235,17 @@ module MarkdownExec
2367
2235
  end
2368
2236
 
2369
2237
  # This method handles the back-link operation in the Markdown execution context.
2370
- # It updates the history state and prepares to load the next block.
2238
+ # It updates the history state for the next block.
2371
2239
  #
2372
- # @return [LoadFileLinkState] An object indicating the action to load the next block.
2373
- def pop_link_history_and_trigger_load
2240
+ # @return [LinkState] An object indicating the state for the next block.
2241
+ def pop_link_history_new_state
2374
2242
  pop = @link_history.pop
2375
2243
  peek = @link_history.peek
2376
- LoadFileLinkState.new(
2377
- LoadFile::LOAD,
2378
- LinkState.new(
2379
- document_filename: pop.document_filename,
2380
- inherited_block_names: peek.inherited_block_names,
2381
- inherited_dependencies: peek.inherited_dependencies,
2382
- inherited_lines: peek.inherited_lines
2383
- )
2244
+ LinkState.new(
2245
+ document_filename: pop.document_filename,
2246
+ inherited_block_names: peek.inherited_block_names,
2247
+ inherited_dependencies: peek.inherited_dependencies,
2248
+ inherited_lines: peek.inherited_lines
2384
2249
  )
2385
2250
  end
2386
2251
 
@@ -2433,8 +2298,7 @@ module MarkdownExec
2433
2298
  %i[blocks line]
2434
2299
  when :line
2435
2300
  unless @delegate_object[:no_chrome]
2436
- create_and_add_chrome_blocks(blocks,
2437
- fcb)
2301
+ create_and_add_chrome_blocks(blocks, fcb)
2438
2302
  end
2439
2303
  end
2440
2304
  end
@@ -2490,8 +2354,6 @@ module MarkdownExec
2490
2354
 
2491
2355
  @allowed_execution_block = @prior_execution_block
2492
2356
  true
2493
- rescue TTY::Reader::InputInterrupt
2494
- exit 1
2495
2357
  end
2496
2358
 
2497
2359
  def prompt_for_command(prompt)
@@ -2560,8 +2422,6 @@ module MarkdownExec
2560
2422
  end
2561
2423
 
2562
2424
  sel == MenuOptions::YES
2563
- rescue TTY::Reader::InputInterrupt
2564
- exit 1
2565
2425
  end
2566
2426
 
2567
2427
  # public
@@ -2590,8 +2450,6 @@ module MarkdownExec
2590
2450
  end
2591
2451
  end
2592
2452
  end
2593
- rescue TTY::Reader::InputInterrupt
2594
- exit 1
2595
2453
  end
2596
2454
 
2597
2455
  def prompt_select_continue(filter: true, quiet: true)
@@ -2605,8 +2463,6 @@ module MarkdownExec
2605
2463
  menu.choice @delegate_object[:prompt_exit]
2606
2464
  end
2607
2465
  sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
2608
- rescue TTY::Reader::InputInterrupt
2609
- exit 1
2610
2466
  end
2611
2467
 
2612
2468
  # user prompt to exit if the menu will be displayed again
@@ -2687,6 +2543,7 @@ module MarkdownExec
2687
2543
  dependencies, selected, next_block_name: next_block_name)
2688
2544
 
2689
2545
  else
2546
+ next_keep_code = link_state&.keep_code || link_block_data.fetch('keep', false) #/*LinkKeys::KEEP*/
2690
2547
  link_history_push_and_next(
2691
2548
  curr_block_name: selected.pub_name,
2692
2549
  curr_document_filename: @delegate_object[:filename],
@@ -2695,8 +2552,10 @@ module MarkdownExec
2695
2552
  inherited_lines: HashDelegator.code_merge(
2696
2553
  link_state&.inherited_lines, code_lines
2697
2554
  ),
2555
+ keep_code: link_state&.keep_code,
2698
2556
  next_block_name: next_block_name,
2699
2557
  next_document_filename: next_document_filename,
2558
+ next_keep_code: next_keep_code,
2700
2559
  next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
2701
2560
  )
2702
2561
  end
@@ -2711,6 +2570,34 @@ module MarkdownExec
2711
2570
  gets.chomp
2712
2571
  end
2713
2572
 
2573
+ def read_saved_assets_for_history_table
2574
+ files = history_files(@dml_link_state).sort
2575
+ files.map do |file|
2576
+ if Regexp.new(@delegate_object[:saved_asset_match]) =~ file
2577
+ begin
2578
+ OpenStruct.new(
2579
+ file: file,
2580
+ row: format(
2581
+ @delegate_object[:saved_history_format],
2582
+ # create with default '*' so unknown parameters are given a wildcard
2583
+ $~.names.each_with_object(Hash.new('*')) do |name, hash|
2584
+ hash[name.to_sym] = $~[name]
2585
+ end
2586
+ )
2587
+ )
2588
+ rescue KeyError
2589
+ # pp $!, $@
2590
+ warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
2591
+ error_handler('saved_history_format')
2592
+ return nil
2593
+ end
2594
+ else
2595
+ warn "Cannot parse name: #{file}"
2596
+ next
2597
+ end
2598
+ end&.compact
2599
+ end
2600
+
2714
2601
  # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2715
2602
  # @param selected [Hash] Selected item from the menu containing a YAML body.
2716
2603
  # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
@@ -2720,12 +2607,12 @@ module MarkdownExec
2720
2607
  obj = {}
2721
2608
 
2722
2609
  # concatenated body of all required blocks loaded a YAML
2723
- data = YAML.load(
2610
+ data = (YAML.load(
2724
2611
  collect_required_code_lines(
2725
2612
  mdoc: mdoc, selected: selected,
2726
2613
  link_state: link_state, block_source: {}
2727
2614
  ).join("\n")
2728
- ).transform_keys(&:to_sym)
2615
+ ) || {}).transform_keys(&:to_sym)
2729
2616
 
2730
2617
  if selected.shell == BlockType::OPTS
2731
2618
  obj = data
@@ -2805,14 +2692,19 @@ module MarkdownExec
2805
2692
  if @delegate_object[exception_sym] != 0
2806
2693
  data = { name: name, detail: items.join(', ') }
2807
2694
  warn(
2808
- format(
2809
- @delegate_object.fetch(:exception_format_name, "\n%{name}"),
2810
- data
2811
- ).send(@delegate_object.fetch(:exception_color_name, :red)) +
2812
- format(
2813
- @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
2814
- data
2815
- ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
2695
+ AnsiString.new(format(
2696
+ @delegate_object.fetch(:exception_format_name,
2697
+ "\n%{name}"),
2698
+ data
2699
+ )).send(@delegate_object.fetch(:exception_color_name,
2700
+ :red)) +
2701
+ AnsiString.new(format(
2702
+ @delegate_object.fetch(:exception_format_detail,
2703
+ " - %{detail}\n"),
2704
+ data
2705
+ )).send(@delegate_object.fetch(
2706
+ :exception_color_detail, :yellow
2707
+ ))
2816
2708
  )
2817
2709
  end
2818
2710
  return unless (@delegate_object[exception_sym]).positive?
@@ -2865,6 +2757,24 @@ module MarkdownExec
2865
2757
  @fout.fout "File saved: #{@run_state.saved_filespec}"
2866
2758
  end
2867
2759
 
2760
+ def select_document_if_multiple(options, files, prompt:)
2761
+ # binding.irb
2762
+ return files if files.class == String ###
2763
+ return files[0] if (count = files.count) == 1
2764
+
2765
+ return unless count >= 2
2766
+
2767
+ opts = options.dup
2768
+ select_option_or_exit(
2769
+ string_send_color(
2770
+ prompt,
2771
+ :prompt_color_after_script_execution
2772
+ ),
2773
+ files,
2774
+ opts.merge(per_page: opts[:select_page_height])
2775
+ )
2776
+ end
2777
+
2868
2778
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
2869
2779
  def select_option_with_metadata(prompt_text, menu_items, opts = {})
2870
2780
  ## configure to environment
@@ -2906,40 +2816,6 @@ module MarkdownExec
2906
2816
  end
2907
2817
 
2908
2818
  selected
2909
- rescue TTY::Reader::InputInterrupt
2910
- exit 1
2911
- rescue StandardError
2912
- HashDelegator.error_handler('select_option_with_metadata')
2913
- end
2914
-
2915
- # Update the block name in the link state and delegate object.
2916
- #
2917
- # This method updates the block name based on whether it was specified
2918
- # through the CLI or derived from the link state.
2919
- #
2920
- # @param link_state [LinkState] The current link state object.
2921
- # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2922
- def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2923
- @delegate_object[:filename] = link_state.document_filename
2924
- link_state.block_name = @delegate_object[:block_name] =
2925
- block_name_from_cli ? @cli_block_name : link_state.block_name
2926
- end
2927
-
2928
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:,
2929
- link_state:)
2930
- block_name_from_cli, now_using_cli =
2931
- manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2932
- now_using_cli: now_using_cli,
2933
- link_state: link_state)
2934
- set_delob_filename_block_name(link_state: link_state,
2935
- block_name_from_cli: block_name_from_cli)
2936
-
2937
- # update @delegate_object and @menu_base_options in auto_load
2938
- #
2939
- blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2940
- dump_delobj(blocks_in_file, menu_blocks, link_state)
2941
-
2942
- [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2943
2819
  end
2944
2820
 
2945
2821
  def set_environment_variables_for_block(selected)
@@ -3026,7 +2902,7 @@ module MarkdownExec
3026
2902
  stdin: if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
3027
2903
  tn.named_captures.sym_keys
3028
2904
  end,
3029
- stdout: if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
2905
+ stdout: if (tn = rest.match(/>(?<type>\$)?(?<name>[\w.\-]+)/))
3030
2906
  tn.named_captures.sym_keys
3031
2907
  end,
3032
2908
  title: title,
@@ -3051,7 +2927,7 @@ module MarkdownExec
3051
2927
  # @param line [String] The current line being processed.
3052
2928
  # @param state [Hash] The current state of the parser, including flags and data related to the processing.
3053
2929
  # @param opts [Hash] A hash containing various options for line and block processing.
3054
- # @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
2930
+ # @param selected_types [Array<String>] Accumulator for lines or messages that are subject to further processing.
3055
2931
  # @param block [Proc] An optional block for further processing or transformation of lines.
3056
2932
  #
3057
2933
  # @option state [Array<String>] :headings Current headings to be updated based on the line.
@@ -3061,9 +2937,9 @@ module MarkdownExec
3061
2937
  #
3062
2938
  # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
3063
2939
  #
3064
- # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
2940
+ # @return [Void] The function modifies the `state` and `selected_types` arguments in place.
3065
2941
  ##
3066
- def update_line_and_block_state(nested_line, state, selected_messages,
2942
+ def update_line_and_block_state(nested_line, state, selected_types,
3067
2943
  &block)
3068
2944
  line = nested_line.to_s
3069
2945
  if line.match(@delegate_object[:fenced_start_and_end_regex])
@@ -3072,9 +2948,9 @@ module MarkdownExec
3072
2948
  #
3073
2949
  HashDelegator.update_menu_attrib_yield_selected(
3074
2950
  fcb: state[:fcb],
3075
- messages: selected_messages,
2951
+ messages: selected_types,
3076
2952
  configuration: @delegate_object,
3077
- &block
2953
+ &block
3078
2954
  )
3079
2955
  state[:in_fenced_block] = false
3080
2956
  else
@@ -3096,7 +2972,7 @@ module MarkdownExec
3096
2972
  elsif nested_line[:depth].zero? || @delegate_object[:menu_include_imported_notes]
3097
2973
  # add line if it is depth 0 or option allows it
3098
2974
  #
3099
- HashDelegator.yield_line_if_selected(line, selected_messages, &block)
2975
+ HashDelegator.yield_line_if_selected(line, selected_types, &block)
3100
2976
 
3101
2977
  else
3102
2978
  # &bsp 'line is not recognized for block state'
@@ -3107,10 +2983,464 @@ module MarkdownExec
3107
2983
  ## apply options to current state
3108
2984
  #
3109
2985
  def update_menu_base(options)
3110
- @menu_base_options.merge!(options)
2986
+ # under simple uses, @menu_base_options may be nil
2987
+ @menu_base_options&.merge!(options)
3111
2988
  @delegate_object.merge!(options)
3112
2989
  end
3113
2990
 
2991
+ def vux_await_user_selection
2992
+ @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
2993
+ menu_blocks: @dml_menu_blocks,
2994
+ default: @dml_menu_default_dname)
2995
+ # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
2996
+ if !@dml_block_state
2997
+ HashDelegator.error_handler('block_state missing', { abort: true })
2998
+ elsif @dml_block_state.state == MenuState::EXIT
2999
+ # &bsp 'load_cli_or_user_selected_block -> break'
3000
+ :break
3001
+ end
3002
+ end
3003
+
3004
+ def vux_clear_menu_state
3005
+ @dml_block_state = SelectedBlockMenuState.new
3006
+ @delegate_object[:block_name] = nil
3007
+ end
3008
+
3009
+ def vux_edit_inherited
3010
+ edited = edit_text(@dml_link_state.inherited_lines_block)
3011
+ @dml_link_state.inherited_lines = edited.split("\n") if edited
3012
+ end
3013
+
3014
+ def vux_execute_and_prompt(block_name)
3015
+ @dml_block_state = block_state_for_name_from_cli(block_name)
3016
+ if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS
3017
+ debounce_reset
3018
+ link_state = LinkState.new
3019
+ options_state = read_show_options_and_trigger_reuse(
3020
+ link_state: link_state,
3021
+ mdoc: @dml_mdoc,
3022
+ selected: @dml_block_state.block
3023
+ )
3024
+
3025
+ update_menu_base(options_state.options)
3026
+ options_state.load_file_link_state.link_state
3027
+ return
3028
+ end
3029
+
3030
+ return :break if execute_block_in_state(block_name) == :break
3031
+
3032
+ if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli,
3033
+ selected: @dml_block_state.block)
3034
+ return :break
3035
+ end
3036
+
3037
+ ## order of block name processing: link block, cli, from user
3038
+ #
3039
+ @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break =
3040
+ HashDelegator.next_link_state(
3041
+ block_name: @dml_link_state.block_name,
3042
+ block_name_from_cli: @dml_now_using_cli,
3043
+ block_state: @dml_block_state,
3044
+ was_using_cli: @dml_now_using_cli
3045
+ )
3046
+
3047
+ # &bsp '!block_name_from_ui + cli_break -> break'
3048
+ !@dml_block_state.source.block_name_from_ui && cli_break && :break
3049
+ end
3050
+
3051
+ def vux_execute_block_per_type(block_name, formatted_choice_ostructs)
3052
+ case block_name
3053
+ when formatted_choice_ostructs[:back].pub_name
3054
+ debounce_reset
3055
+ vux_navigate_back_for_ls
3056
+
3057
+ when formatted_choice_ostructs[:edit].pub_name
3058
+ debounce_reset
3059
+ vux_edit_inherited
3060
+ return :break if pause_user_exit
3061
+
3062
+ InputSequencer.next_link_state(prior_block_was_link: true)
3063
+
3064
+ when formatted_choice_ostructs[:history].pub_name
3065
+ debounce_reset
3066
+ files_table_rows = read_saved_assets_for_history_table
3067
+ return :break unless files_table_rows
3068
+
3069
+ execute_history_select(files_table_rows, stream: $stderr)
3070
+ return :break if pause_user_exit
3071
+
3072
+ InputSequencer.next_link_state(prior_block_was_link: true)
3073
+
3074
+ when formatted_choice_ostructs[:load].pub_name
3075
+ debounce_reset
3076
+ vux_load_inherited
3077
+ return :break if pause_user_exit
3078
+
3079
+ InputSequencer.next_link_state(prior_block_was_link: true)
3080
+
3081
+ when formatted_choice_ostructs[:save].pub_name
3082
+ debounce_reset
3083
+ return :break if execute_inherited_save == :break
3084
+
3085
+ InputSequencer.next_link_state(prior_block_was_link: true)
3086
+
3087
+ when formatted_choice_ostructs[:shell].pub_name
3088
+ debounce_reset
3089
+ vux_input_and_execute_shell_commands(stream: $stderr)
3090
+ return :break if pause_user_exit
3091
+
3092
+ InputSequencer.next_link_state(prior_block_was_link: true)
3093
+
3094
+ when formatted_choice_ostructs[:view].pub_name
3095
+ debounce_reset
3096
+ vux_view_inherited(stream: $stderr)
3097
+ return :break if pause_user_exit
3098
+
3099
+ InputSequencer.next_link_state(prior_block_was_link: true)
3100
+
3101
+ else
3102
+ return :break if vux_execute_and_prompt(block_name) == :break
3103
+
3104
+ InputSequencer.next_link_state(
3105
+ block_name: @dml_link_state.block_name,
3106
+ prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH
3107
+ )
3108
+ end
3109
+ end
3110
+
3111
+ def vux_formatted_names_for_state_chrome_blocks(
3112
+ names: %w[back edit history load save shell view]
3113
+ )
3114
+ names.each_with_object({}) do |name, result|
3115
+ do_key = :"menu_option_#{name}_name"
3116
+ oname = HashDelegator.safeval(@delegate_object[do_key])
3117
+ dname = format(@delegate_object[:menu_link_format], oname)
3118
+ result[name.to_sym] = OpenStruct.new(
3119
+ dname: dname,
3120
+ name: dname,
3121
+ oname: dname,
3122
+ pub_name: dname.pub_name
3123
+ )
3124
+ end
3125
+ end
3126
+
3127
+ def vux_init
3128
+ @menu_base_options = @delegate_object
3129
+ @dml_link_state = LinkState.new(
3130
+ block_name: @delegate_object[:block_name],
3131
+ document_filename: @delegate_object[:filename]
3132
+ )
3133
+ @run_state.source.block_name_from_cli = @dml_link_state.block_name.present?
3134
+ @cli_block_name = @dml_link_state.block_name
3135
+ @dml_now_using_cli = @run_state.source.block_name_from_cli
3136
+ @dml_menu_default_dname = nil
3137
+ @dml_block_state = SelectedBlockMenuState.new
3138
+ @doc_saved_lines_files = []
3139
+
3140
+ @run_state.batch_random = Random.new.rand
3141
+ @run_state.batch_index = 0
3142
+
3143
+ @run_state.files = StreamsOut.new
3144
+ end
3145
+
3146
+ def vux_input_and_execute_shell_commands(stream:)
3147
+ loop do
3148
+ command = prompt_for_command(AnsiString.new(":MDE #{Time.now.strftime('%FT%TZ')}> ").send(:bgreen))
3149
+ break if !command.present? || command == 'exit'
3150
+
3151
+ exit_status = execute_command_with_streams(
3152
+ [@delegate_object[:shell], '-c', command]
3153
+ )
3154
+ case exit_status
3155
+ when 0
3156
+ stream.puts "#{'OK'.green} #{exit_status}"
3157
+ else
3158
+ stream.puts "#{'ERR'.bred} #{exit_status}"
3159
+ end
3160
+ end
3161
+ end
3162
+
3163
+ ## load file with code lines per options
3164
+ #
3165
+ def vux_load_code_files_into_state
3166
+ return unless @menu_base_options[:load_code].present?
3167
+
3168
+ @dml_link_state.inherited_lines =
3169
+ @menu_base_options[:load_code].split(':').map do |path|
3170
+ File.readlines(path, chomp: true)
3171
+ end.flatten(1)
3172
+
3173
+ inherited_block_names = []
3174
+ inherited_dependencies = {}
3175
+ selected = FCB.new(oname: 'load_code')
3176
+ pop_add_current_code_to_head_and_trigger_load(
3177
+ @dml_link_state, inherited_block_names,
3178
+ code_lines, inherited_dependencies, selected
3179
+ )
3180
+ end
3181
+
3182
+ def vux_load_inherited
3183
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
3184
+ @delegate_object[:document_saved_lines_glob])
3185
+ load_filespec = load_filespec_from_expression(sf)
3186
+ return unless load_filespec
3187
+
3188
+ @dml_link_state.inherited_lines_append(
3189
+ File.readlines(load_filespec, chomp: true)
3190
+ )
3191
+ end
3192
+
3193
+ # Select and execute a code block from a Markdown document.
3194
+ #
3195
+ # This method allows the user to interactively select a code block from a
3196
+ # Markdown document, obtain approval, and execute the chosen block of code.
3197
+ #
3198
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
3199
+ def vux_main_loop
3200
+ vux_init
3201
+ vux_load_code_files_into_state
3202
+ formatted_choice_ostructs = vux_formatted_names_for_state_chrome_blocks
3203
+
3204
+ block_list = [@delegate_object[:block_name]].select(&:present?).compact + @delegate_object[:input_cli_rest]
3205
+ @delegate_object[:block_name] = nil
3206
+
3207
+ process_commands(
3208
+ arguments: @p_all_arguments,
3209
+ named_procs: yield(:command_names, @delegate_object),
3210
+ options_parsed: @p_options_parsed,
3211
+ rest: @p_rest,
3212
+ enable_search: @delegate_object[:default_find_select_open]
3213
+ ) do |type, data|
3214
+ case type
3215
+ when ArgPro::ActSetBlockName
3216
+ @delegate_object[:block_name] = data
3217
+ @delegate_object[:input_cli_rest] = ''
3218
+ when ArgPro::ConvertValue
3219
+ # call for side effects, output, or exit
3220
+ data[0].call(data[1])
3221
+ when ArgPro::ActFileIsMissing
3222
+ raise FileMissingError, data, caller
3223
+ when ArgPro::ActFind
3224
+ find_value(data, execute_chosen_found: true)
3225
+ when ArgPro::ActSetFileName
3226
+ @delegate_object[:filename] = data
3227
+ when ArgPro::ActSetPath
3228
+ @delegate_object[:path] = data
3229
+ when ArgPro::CallProcess
3230
+ yield :call_proc, [@delegate_object, data]
3231
+ when ArgPro::ActSetOption
3232
+ @delegate_object[data[0]] = data[1]
3233
+ else
3234
+ raise
3235
+ end
3236
+ end
3237
+
3238
+ InputSequencer.new(
3239
+ @delegate_object[:filename],
3240
+ block_list
3241
+ ).run do |msg, data|
3242
+ # &bt msg
3243
+ case msg
3244
+ when :parse_document # once for each menu
3245
+ vux_parse_document
3246
+ vux_menu_append_history_files(formatted_choice_ostructs)
3247
+ vux_publish_document_file_name_for_external_automation
3248
+
3249
+ when :display_menu
3250
+ vux_clear_menu_state
3251
+
3252
+ when :user_choice
3253
+ vux_user_selected_block_name
3254
+
3255
+ when :execute_block
3256
+ ret = vux_execute_block_per_type(data, formatted_choice_ostructs)
3257
+ vux_publish_block_name_for_external_automation(data)
3258
+ ret
3259
+
3260
+ when :close_ux
3261
+ if @vux_pipe_open.present? && File.exist?(@vux_pipe_open)
3262
+ @vux_pipe_open.close
3263
+ @vux_pipe_open = nil
3264
+ end
3265
+ if @vux_pipe_created.present? && File.exist?(@vux_pipe_created)
3266
+ File.delete(@vux_pipe_created)
3267
+ @vux_pipe_created = nil
3268
+ end
3269
+
3270
+ when :exit?
3271
+ data == $texit
3272
+
3273
+ when :stay?
3274
+ data == $stay
3275
+
3276
+ else
3277
+ raise "Invalid message: #{msg}"
3278
+
3279
+ end
3280
+ end
3281
+ end
3282
+
3283
+ def vux_menu_append_history_files(formatted_choice_ostructs)
3284
+ if @delegate_object[:menu_for_history]
3285
+ history_files(@dml_link_state).tap do |files|
3286
+ if files.count.positive?
3287
+ dml_menu_append_chrome_item(
3288
+ formatted_choice_ostructs[:history].oname, files.count,
3289
+ 'files', menu_state: MenuState::HISTORY
3290
+ )
3291
+ end
3292
+ end
3293
+ end
3294
+
3295
+ return unless @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
3296
+
3297
+ sf = document_name_in_glob_as_file_name(
3298
+ @dml_link_state.document_filename,
3299
+ @delegate_object[:document_saved_lines_glob]
3300
+ )
3301
+ files = sf ? Dir.glob(sf) : []
3302
+ @doc_saved_lines_files = files.count.positive? ? files : []
3303
+
3304
+ lines_count = @dml_link_state.inherited_lines_count
3305
+
3306
+ # add menu items (glob, load, save) and enable selectively
3307
+ if files.count.positive? || lines_count.positive?
3308
+ menu_add_disabled_option(sf)
3309
+ end
3310
+ if files.count.positive?
3311
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:load].dname, files.count, 'files',
3312
+ menu_state: MenuState::LOAD)
3313
+ end
3314
+ if @delegate_object[:menu_inherited_lines_edit_always] || lines_count.positive?
3315
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:edit].dname, lines_count, 'lines',
3316
+ menu_state: MenuState::EDIT)
3317
+ end
3318
+ if lines_count.positive?
3319
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:save].dname, 1, '',
3320
+ menu_state: MenuState::SAVE)
3321
+ end
3322
+ if lines_count.positive?
3323
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:view].dname, 1, '',
3324
+ menu_state: MenuState::VIEW)
3325
+ end
3326
+ # rubocop:disable Style/GuardClause
3327
+ if @delegate_object[:menu_with_shell]
3328
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:shell].dname, 1, '',
3329
+ menu_state: MenuState::SHELL)
3330
+ end
3331
+ # rubocop:enable Style/GuardClause
3332
+
3333
+ # # reflect new menu items
3334
+ # @dml_mdoc = MDoc.new(@dml_menu_blocks)
3335
+ end
3336
+
3337
+ def vux_navigate_back_for_ls
3338
+ InputSequencer.merge_link_state(
3339
+ @dml_link_state,
3340
+ InputSequencer.next_link_state(
3341
+ **execute_navigate_back.merge(prior_block_was_link: true)
3342
+ )
3343
+ )
3344
+ end
3345
+
3346
+ def vux_parse_document
3347
+ @run_state.batch_index += 1
3348
+ @run_state.in_own_window = false
3349
+
3350
+ @run_state.source.block_name_from_cli, @dml_now_using_cli =
3351
+ manage_cli_selection_state(
3352
+ block_name_from_cli: @run_state.source.block_name_from_cli,
3353
+ now_using_cli: @dml_now_using_cli,
3354
+ link_state: @dml_link_state
3355
+ )
3356
+
3357
+ @delegate_object[:filename] = @dml_link_state.document_filename
3358
+ @dml_link_state.block_name = @delegate_object[:block_name] =
3359
+ @run_state.source.block_name_from_cli ?
3360
+ @cli_block_name :
3361
+ @dml_link_state.block_name
3362
+
3363
+ # update @delegate_object and @menu_base_options in auto_load
3364
+ #
3365
+ @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
3366
+ mdoc_menu_and_blocks_from_nested_files(@dml_link_state)
3367
+ dump_delobj(@dml_blocks_in_file, @dml_menu_blocks, @dml_link_state)
3368
+ # &bsp 'loop', @run_state.source.block_name_from_cli, @cli_block_name
3369
+ end
3370
+
3371
+ def publish_for_external_automation(message:)
3372
+ return if @delegate_object[:publish_document_file_name].empty?
3373
+
3374
+ pipe_path = absolute_path(@delegate_object[:publish_document_file_name])
3375
+
3376
+ case @delegate_object[:publish_document_file_mode]
3377
+ when 'append'
3378
+ File.write(pipe_path, message + "\n", mode: 'a')
3379
+ when 'fifo'
3380
+ unless @vux_pipe_open
3381
+ unless File.exist?(pipe_path)
3382
+ FileUtils.mkfifo(pipe_path)
3383
+ @vux_pipe_created = pipe_path
3384
+ end
3385
+ @vux_pipe_open = File.open(pipe_path, 'w')
3386
+ end
3387
+ @vux_pipe_open.puts(message + "\n")
3388
+ @vux_pipe_open.flush
3389
+ when 'write'
3390
+ File.write(pipe_path, message)
3391
+ else
3392
+ raise 'Invalid publish_document_file_mode:' \
3393
+ " #{@delegate_object[:publish_document_file_mode]}"
3394
+ end
3395
+ end
3396
+
3397
+ def vux_publish_block_name_for_external_automation(block_name)
3398
+ publish_for_external_automation(
3399
+ message: format(
3400
+ @delegate_object[:publish_block_name_format],
3401
+ { block: block_name,
3402
+ document: @delegate_object[:filename],
3403
+ time: Time.now.utc.strftime(
3404
+ @delegate_object[:publish_time_format]
3405
+ ) }
3406
+ )
3407
+ )
3408
+ end
3409
+
3410
+ def vux_publish_document_file_name_for_external_automation
3411
+ return unless @delegate_object[:publish_document_file_name].present?
3412
+
3413
+ publish_for_external_automation(
3414
+ message: format(
3415
+ @delegate_object[:publish_document_name_format],
3416
+ { document: @delegate_object[:filename],
3417
+ time: Time.now.utc.strftime(
3418
+ @delegate_object[:publish_time_format]
3419
+ ) }
3420
+ )
3421
+ )
3422
+ end
3423
+
3424
+ # return :break to break from loop
3425
+ def vux_user_selected_block_name
3426
+ if @dml_link_state.block_name.present?
3427
+ # @prior_block_was_link = true
3428
+ @dml_block_state.block = blocks_find_by_block_name(@dml_blocks_in_file,
3429
+ @dml_link_state.block_name)
3430
+ @dml_link_state.block_name = nil
3431
+ else
3432
+ # puts "? - Select a block to execute (or type #{$texit} to exit):"
3433
+ return :break if vux_await_user_selection == :break # into @dml_block_state
3434
+ return :break if @dml_block_state.block.nil? # no block matched
3435
+ end
3436
+ # puts "! - Executing block: #{data}"
3437
+ @dml_block_state.block&.pub_name
3438
+ end
3439
+
3440
+ def vux_view_inherited(stream:)
3441
+ stream.puts @dml_link_state.inherited_lines_block
3442
+ end
3443
+
3114
3444
  def wait_for_stream_processing
3115
3445
  @process_mutex.synchronize do
3116
3446
  @process_cv.wait(@process_mutex)
@@ -3123,11 +3453,13 @@ module MarkdownExec
3123
3453
  block_state = wait_for_user_selection(all_blocks, menu_blocks, default)
3124
3454
  handle_back_or_continue(block_state)
3125
3455
  block_state
3126
- rescue StandardError
3127
- HashDelegator.error_handler('wait_for_user_selected_block')
3128
3456
  end
3129
3457
 
3130
3458
  def wait_for_user_selection(_all_blocks, menu_blocks, default)
3459
+ if @delegate_object[:clear_screen_for_select_block]
3460
+ printf("\e[1;1H\e[2J")
3461
+ end
3462
+
3131
3463
  prompt_title = string_send_color(
3132
3464
  @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution
3133
3465
  )
@@ -3496,7 +3828,8 @@ module MarkdownExec
3496
3828
 
3497
3829
  def test_block_find_with_default
3498
3830
  blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')]
3499
- result = HashDelegator.block_find(blocks, :text, 'missing_value', 'default')
3831
+ result = HashDelegator.block_find(blocks, :text, 'missing_value',
3832
+ 'default')
3500
3833
  assert_equal 'default', result
3501
3834
  end
3502
3835
  end
@@ -3780,31 +4113,31 @@ module MarkdownExec
3780
4113
  @hd.instance_variable_set(:@run_state, mock('run_state'))
3781
4114
  end
3782
4115
 
3783
- def test_format_execution_stream_with_valid_key
3784
- result = HashDelegator.format_execution_stream(
3785
- { stdout: %w[output1 output2] },
3786
- ExecutionStreams::STD_OUT
3787
- )
4116
+ # def test_format_execution_stream_with_valid_key
4117
+ # result = HashDelegator.format_execution_stream(
4118
+ # { stdout: %w[output1 output2] },
4119
+ # ExecutionStreams::STD_OUT
4120
+ # )
3788
4121
 
3789
- assert_equal "output1\noutput2", result
3790
- end
4122
+ # assert_equal "output1\noutput2", result
4123
+ # end
3791
4124
 
3792
- def test_format_execution_stream_with_empty_key
3793
- @hd.instance_variable_get(:@run_state).stubs(:files).returns({})
4125
+ # def test_format_execution_stream_with_empty_key
4126
+ # @hd.instance_variable_get(:@run_state).stubs(:files).returns({})
3794
4127
 
3795
- result = HashDelegator.format_execution_stream(nil,
3796
- ExecutionStreams::STD_ERR)
4128
+ # result = HashDelegator.format_execution_stream(nil,
4129
+ # ExecutionStreams::STD_ERR)
3797
4130
 
3798
- assert_equal '', result
3799
- end
4131
+ # assert_equal '', result
4132
+ # end
3800
4133
 
3801
- def test_format_execution_stream_with_nil_files
3802
- @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil)
4134
+ # def test_format_execution_stream_with_nil_files
4135
+ # @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil)
3803
4136
 
3804
- result = HashDelegator.format_execution_stream(nil, :stdin)
4137
+ # result = HashDelegator.format_execution_stream(nil, :stdin)
3805
4138
 
3806
- assert_equal '', result
3807
- end
4139
+ # assert_equal '', result
4140
+ # end
3808
4141
  end
3809
4142
 
3810
4143
  class TestHashDelegatorHandleBackLink < Minitest::Test
@@ -3813,16 +4146,14 @@ module MarkdownExec
3813
4146
  @hd.stubs(:history_state_pop)
3814
4147
  end
3815
4148
 
3816
- def test_pop_link_history_and_trigger_load
4149
+ def test_pop_link_history_new_state
3817
4150
  # Verifying that history_state_pop is called
3818
4151
  # @hd.expects(:history_state_pop).once
3819
4152
 
3820
- result = @hd.pop_link_history_and_trigger_load
4153
+ result = @hd.pop_link_history_new_state
3821
4154
 
3822
- # Asserting the result is an instance of LoadFileLinkState
3823
- assert_instance_of LoadFileLinkState, result
3824
- assert_equal LoadFile::LOAD, result.load_file
3825
- assert_nil result.link_state.block_name
4155
+ # Asserting the result is an instance of LinkState
4156
+ assert_nil result.block_name
3826
4157
  end
3827
4158
  end
3828
4159
 
@@ -3909,7 +4240,7 @@ module MarkdownExec
3909
4240
  def setup
3910
4241
  @hd = HashDelegator.new
3911
4242
  @hd.instance_variable_set(:@run_state,
3912
- OpenStruct.new(files: { stdout: [] }))
4243
+ OpenStruct.new(files: StreamsOut.new))
3913
4244
  @hd.instance_variable_set(:@delegate_object,
3914
4245
  { output_stdout: true })
3915
4246
  end
@@ -3921,9 +4252,8 @@ module MarkdownExec
3921
4252
  Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
3922
4253
 
3923
4254
  @hd.wait_for_stream_processing
3924
-
3925
4255
  assert_equal ['line 1', 'line 2'],
3926
- @hd.instance_variable_get(:@run_state).files[ExecutionStreams::STD_OUT]
4256
+ @hd.instance_variable_get(:@run_state).files.stream_lines(ExecutionStreams::STD_OUT)
3927
4257
  end
3928
4258
 
3929
4259
  def test_handle_stream_with_io_error
@@ -3936,7 +4266,7 @@ module MarkdownExec
3936
4266
  @hd.wait_for_stream_processing
3937
4267
 
3938
4268
  assert_equal [],
3939
- @hd.instance_variable_get(:@run_state).files[ExecutionStreams::STD_OUT]
4269
+ @hd.instance_variable_get(:@run_state).files.stream_lines(ExecutionStreams::STD_OUT)
3940
4270
  end
3941
4271
  end
3942
4272
 
@@ -3954,9 +4284,9 @@ module MarkdownExec
3954
4284
  def test_iter_blocks_from_nested_files
3955
4285
  @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'],
3956
4286
  import_paths: nil)
3957
- selected_messages = ['filtered message']
4287
+ selected_types = ['filtered message']
3958
4288
 
3959
- result = @hd.iter_blocks_from_nested_files { selected_messages }
4289
+ result = @hd.iter_blocks_from_nested_files { selected_types }
3960
4290
  assert_equal ['line 1', 'line 2'], result
3961
4291
 
3962
4292
  @hd.cfile.verify
@@ -3981,11 +4311,11 @@ module MarkdownExec
3981
4311
  })
3982
4312
  @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('-- Back --')
3983
4313
  @hd.stubs(:string_send_color).with('-- Back --',
3984
- :menu_chrome_color).returns('-- Back --'.red)
4314
+ :menu_chrome_color).returns(AnsiString.new('-- Back --').red)
3985
4315
  end
3986
4316
 
3987
4317
  def test_menu_chrome_colored_option_with_color
3988
- assert_equal '-- Back --'.red,
4318
+ assert_equal AnsiString.new('-- Back --').red,
3989
4319
  @hd.menu_chrome_colored_option(:menu_option_back_name)
3990
4320
  end
3991
4321
 
@@ -4049,10 +4379,11 @@ module MarkdownExec
4049
4379
  end
4050
4380
 
4051
4381
  def test_string_send_color
4052
- assert_equal 'Hello'.red, @hd.string_send_color('Hello', :red)
4053
- assert_equal 'World'.green,
4382
+ assert_equal AnsiString.new('Hello').red,
4383
+ @hd.string_send_color('Hello', :red)
4384
+ assert_equal AnsiString.new('World').green,
4054
4385
  @hd.string_send_color('World', :green)
4055
- assert_equal 'Default'.plain,
4386
+ assert_equal AnsiString.new('Default').plain,
4056
4387
  @hd.string_send_color('Default', :blue)
4057
4388
  end
4058
4389
  end
@@ -4242,10 +4573,7 @@ module MarkdownExec
4242
4573
 
4243
4574
  def test_prompt_for_filespec_with_interruption
4244
4575
  $stdin = StringIO.new
4245
- # rubocop disable:Lint/NestedMethodDefinition
4246
4576
  def $stdin.gets; raise Interrupt; end
4247
- # rubocop enable:Lint/NestedMethodDefinition
4248
-
4249
4577
  result = prompt_for_filespec_with_wildcard('*.txt')
4250
4578
  assert_nil result
4251
4579
  end