markdown_exec 2.2.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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