markdown_exec 2.0.8.4 → 2.2.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.
@@ -31,11 +31,15 @@ require_relative 'fout'
31
31
  require_relative 'hash'
32
32
  require_relative 'link_history'
33
33
  require_relative 'mdoc'
34
+ require_relative 'namer'
34
35
  require_relative 'regexp'
35
36
  require_relative 'resize_terminal'
36
37
  require_relative 'std_out_err_logger'
38
+ require_relative 'streams_out'
37
39
  require_relative 'string_util'
38
40
 
41
+ $pd = false unless defined?($pd)
42
+
39
43
  class String
40
44
  # Checks if the string is not empty.
41
45
  # @return [Boolean] Returns true if the string is not empty, false otherwise.
@@ -53,7 +57,8 @@ module HashDelegatorSelf
53
57
  # @param color_key [String, Symbol] The key representing the desired color method in the color_methods hash.
54
58
  # @param default_method [String] (optional) Default color method to use if color_key is not found in color_methods. Defaults to 'plain'.
55
59
  # @return [String] The colored string.
56
- def apply_color_from_hash(string, color_methods, color_key, default_method: 'plain')
60
+ def apply_color_from_hash(string, color_methods, color_key,
61
+ default_method: 'plain')
57
62
  color_method = color_methods.fetch(color_key, default_method).to_sym
58
63
  string.to_s.send(color_method)
59
64
  end
@@ -77,17 +82,17 @@ module HashDelegatorSelf
77
82
  # colored_string = apply_color_from_hash(string, color_transformations, :red)
78
83
  # puts colored_string # This will print the string in red
79
84
 
80
- # Searches for the first element in a collection where the specified key matches a given value.
85
+ # Searches for the first element in a collection where the specified message sent to an element matches a given value.
81
86
  # This method is particularly useful for finding a specific hash-like object within an enumerable collection.
82
87
  # If no match is found, it returns a specified default value.
83
88
  #
84
89
  # @param blocks [Enumerable] The collection of hash-like objects to search.
85
- # @param key [Object] The key to search for in each element of the collection.
86
- # @param value [Object] The value to match against each element's corresponding key value.
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.
87
92
  # @param default [Object, nil] The default value to return if no match is found (optional).
88
93
  # @return [Object, nil] The first matching element or the default value if no match is found.
89
- def block_find(blocks, key, value, default = nil)
90
- blocks.find { |item| item[key] == value } || default
94
+ def block_find(blocks, msg, value, default = nil)
95
+ blocks.find { |item| item.send(msg) == value } || default
91
96
  end
92
97
 
93
98
  def code_merge(*bodies)
@@ -131,8 +136,10 @@ module HashDelegatorSelf
131
136
  # delete the current line if it is empty and the previous is also empty
132
137
  def delete_consecutive_blank_lines!(blocks_menu)
133
138
  blocks_menu.process_and_conditionally_delete! do |prev_item, current_item, _next_item|
134
- prev_item&.fetch(:chrome, nil) && !prev_item&.fetch(:oname).present? &&
135
- current_item&.fetch(:chrome, nil) && !current_item&.fetch(:oname).present?
139
+ prev_item&.fetch(:chrome, nil) &&
140
+ !(prev_item && prev_item.oname.present?) &&
141
+ current_item&.fetch(:chrome, nil) &&
142
+ !(current_item && current_item.oname.present?)
136
143
  end
137
144
  end
138
145
 
@@ -163,15 +170,6 @@ module HashDelegatorSelf
163
170
  # end.join("\n")
164
171
  # end
165
172
 
166
- # Formats and returns the execution streams (like stdin, stdout, stderr) for a given key.
167
- # It concatenates the array of strings found under the specified key in the run_state's files.
168
- #
169
- # @param key [Symbol] The key corresponding to the desired execution stream.
170
- # @return [String] A concatenated string of the execution stream's contents.
171
- def format_execution_streams(key, files = {})
172
- (files || {}).fetch(key, []).join
173
- end
174
-
175
173
  # Indents all lines in a given string with a specified indentation string.
176
174
  # @param body [String] A multi-line string to be indented.
177
175
  # @param indent [String] The string used for indentation (default is an empty string).
@@ -196,13 +194,14 @@ module HashDelegatorSelf
196
194
  merged.empty? ? [] : merged
197
195
  end
198
196
 
199
- def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, block_name: nil)
197
+ def next_link_state(block_name_from_cli:, was_using_cli:, block_state:,
198
+ block_name: nil)
200
199
  # Set block_name based on block_name_from_cli
201
200
  block_name = @cli_block_name if block_name_from_cli
202
201
 
203
202
  # Determine the state of breaker based on was_using_cli and the block type
204
- # true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block[:shell] equals BlockType::BASH. In all other scenarios, breaker is false.
205
- breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
203
+ # true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block.shell equals BlockType::BASH. In all other scenarios, breaker is false.
204
+ breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.shell == BlockType::BASH
206
205
 
207
206
  # Reset block_name_from_cli if the conditions are not met
208
207
  block_name_from_cli ||= false
@@ -266,16 +265,6 @@ module HashDelegatorSelf
266
265
  exit 1
267
266
  end
268
267
 
269
- # # Evaluates the given string as Ruby code and rescues any StandardErrors.
270
- # # If an error occurs, it calls the error_handler method with 'safeval'.
271
- # # @param str [String] The string to be evaluated.
272
- # # @return [Object] The result of evaluating the string.
273
- # def safeval(str)
274
- # eval(str)
275
- # rescue StandardError # catches NameError, StandardError
276
- # error_handler('safeval')
277
- # end
278
-
279
268
  def set_file_permissions(file_path, chmod_value)
280
269
  File.chmod(chmod_value, file_path)
281
270
  end
@@ -300,7 +289,8 @@ module HashDelegatorSelf
300
289
  # @param fcb [Object] The fcb object whose attributes are to be updated.
301
290
  # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable.
302
291
  # @param block [Block] An optional block to yield to if conditions are met.
303
- def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, &block)
292
+ def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {},
293
+ &block)
304
294
  initialize_fcb_names(fcb)
305
295
  return unless fcb.body
306
296
 
@@ -309,21 +299,6 @@ module HashDelegatorSelf
309
299
  &block)
310
300
  end
311
301
 
312
- def write_execution_output_to_file(files, filespec)
313
- FileUtils.mkdir_p File.dirname(filespec)
314
-
315
- File.write(
316
- filespec,
317
- ["-STDOUT-\n",
318
- format_execution_streams(ExecutionStreams::STD_OUT, files),
319
- "-STDERR-\n",
320
- format_execution_streams(ExecutionStreams::STD_ERR, files),
321
- "-STDIN-\n",
322
- format_execution_streams(ExecutionStreams::STD_IN, files),
323
- "\n"].join
324
- )
325
- end
326
-
327
302
  # Yields a line as a new block if the selected message type includes :line.
328
303
  # @param [String] line The line to be processed.
329
304
  # @param [Array<Symbol>] selected_messages A list of message types to check.
@@ -462,7 +437,9 @@ class StringWrapper
462
437
  words.each.with_index do |word, index|
463
438
  trial_length = word.length
464
439
  trial_length += @first_indent.length if index.zero?
465
- trial_length += current_line.length + 1 + @rest_indent.length if index != 0
440
+ if index != 0
441
+ trial_length += current_line.length + 1 + @rest_indent.length
442
+ end
466
443
  if trial_length > max_line_length && (words.count != 0)
467
444
  lines << current_line
468
445
  current_line = word
@@ -513,7 +490,8 @@ module MarkdownExec
513
490
  @most_recent_loaded_filename = nil
514
491
  @pass_args = []
515
492
  @run_state = OpenStruct.new(
516
- link_history: []
493
+ link_history: [],
494
+ source: OpenStruct.new
517
495
  )
518
496
  @link_history = LinkHistory.new
519
497
  @fout = FOut.new(@delegate_object) ### slice only relevant keys
@@ -539,13 +517,18 @@ module MarkdownExec
539
517
  def add_menu_chrome_blocks!(menu_blocks:, link_state:)
540
518
  return unless @delegate_object[:menu_link_format].present?
541
519
 
542
- add_inherited_lines(menu_blocks: menu_blocks, link_state: link_state) if @delegate_object[:menu_with_inherited_lines]
520
+ if @delegate_object[:menu_with_inherited_lines]
521
+ add_inherited_lines(menu_blocks: menu_blocks,
522
+ link_state: link_state)
523
+ end
543
524
 
544
525
  # back before exit
545
526
  add_back_option(menu_blocks: menu_blocks) if should_add_back_option?
546
527
 
547
528
  # exit after other options
548
- add_exit_option(menu_blocks: menu_blocks) if @delegate_object[:menu_with_exit]
529
+ if @delegate_object[:menu_with_exit]
530
+ add_exit_option(menu_blocks: menu_blocks)
531
+ end
549
532
 
550
533
  add_dividers(menu_blocks: menu_blocks)
551
534
  end
@@ -587,6 +570,9 @@ module MarkdownExec
587
570
  when MenuState::EXIT
588
571
  option_name = @delegate_object[:menu_option_exit_name]
589
572
  insert_at_top = @delegate_object[:menu_exit_at_top]
573
+ when MenuState::HISTORY
574
+ option_name = @delegate_object[:menu_option_history_name]
575
+ insert_at_top = @delegate_object[:menu_load_at_top]
590
576
  when MenuState::LOAD
591
577
  option_name = @delegate_object[:menu_option_load_name]
592
578
  insert_at_top = @delegate_object[:menu_load_at_top]
@@ -599,6 +585,8 @@ module MarkdownExec
599
585
  when MenuState::VIEW
600
586
  option_name = @delegate_object[:menu_option_view_name]
601
587
  insert_at_top = @delegate_object[:menu_load_at_top]
588
+ else
589
+ raise "Missing MenuState: #{menu_state}"
602
590
  end
603
591
 
604
592
  formatted_name = format(@delegate_object[:menu_link_format],
@@ -616,6 +604,20 @@ module MarkdownExec
616
604
  else
617
605
  menu_blocks.push(chrome_block)
618
606
  end
607
+
608
+ chrome_block
609
+ end
610
+
611
+ # Appends a formatted divider to the specified position in a menu block array.
612
+ # The method checks for the presence of formatting options before appending.
613
+ #
614
+ # @param menu_blocks [Array] The array of menu block elements.
615
+ # @param position [Symbol] The position to insert the divider (:initial or :final).
616
+ def append_divider(menu_blocks:, position:)
617
+ return unless divider_formatting_present?(position)
618
+
619
+ divider = create_divider(position)
620
+ position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider)
619
621
  end
620
622
 
621
623
  # Appends a formatted divider to the specified position in a menu block array.
@@ -624,10 +626,10 @@ module MarkdownExec
624
626
  # @param menu_blocks [Array] The array of menu block elements.
625
627
  # @param position [Symbol] The position to insert the divider (:initial or :final).
626
628
  def append_inherited_lines(menu_blocks:, link_state:, position: top)
627
- return unless link_state.inherited_lines.present?
629
+ return unless link_state.inherited_lines_present?
628
630
 
629
631
  insert_at_top = @delegate_object[:menu_inherited_lines_at_top]
630
- chrome_blocks = link_state.inherited_lines.map do |line|
632
+ chrome_blocks = link_state.inherited_lines_map do |line|
631
633
  formatted = format(@delegate_object[:menu_inherited_lines_format],
632
634
  { line: line })
633
635
  FCB.new(
@@ -651,18 +653,6 @@ module MarkdownExec
651
653
  HashDelegator.error_handler('append_inherited_lines')
652
654
  end
653
655
 
654
- # Appends a formatted divider to the specified position in a menu block array.
655
- # The method checks for the presence of formatting options before appending.
656
- #
657
- # @param menu_blocks [Array] The array of menu block elements.
658
- # @param position [Symbol] The position to insert the divider (:initial or :final).
659
- def append_divider(menu_blocks:, position:)
660
- return unless divider_formatting_present?(position)
661
-
662
- divider = create_divider(position)
663
- position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider)
664
- end
665
-
666
656
  # private
667
657
 
668
658
  # Applies shell color options to the given string if applicable.
@@ -700,7 +690,7 @@ module MarkdownExec
700
690
  iter_blocks_from_nested_files do |btype, fcb|
701
691
  process_block_based_on_type(blocks, btype, fcb)
702
692
  end
703
- # &bc 'blocks.count:', blocks.count
693
+ # &bt blocks.count
704
694
  blocks
705
695
  rescue StandardError
706
696
  HashDelegator.error_handler('blocks_from_nested_files')
@@ -712,7 +702,8 @@ module MarkdownExec
712
702
  SelectedBlockMenuState.new(
713
703
  @dml_blocks_in_file.find do |item|
714
704
  block_name == item.pub_name
715
- end&.merge(
705
+ end,
706
+ OpenStruct.new(
716
707
  block_name_from_cli: true,
717
708
  block_name_from_ui: false
718
709
  ),
@@ -726,11 +717,14 @@ module MarkdownExec
726
717
  return unless @delegate_object[:saved_stdout_folder]
727
718
 
728
719
  @delegate_object[:logged_stdout_filename] =
729
- SavedAsset.stdout_name(blockname: block_name,
730
- filename: File.basename(@delegate_object[:filename],
731
- '.*'),
732
- prefix: @delegate_object[:logged_stdout_filename_prefix],
733
- time: Time.now.utc)
720
+ SavedAsset.new(
721
+ blockname: block_name,
722
+ filename: @delegate_object[:filename],
723
+ prefix: @delegate_object[:logged_stdout_filename_prefix],
724
+ time: Time.now.utc,
725
+ exts: '.out.txt',
726
+ saved_asset_format: shell_escape_asset_format(@dml_link_state)
727
+ ).generate_name
734
728
 
735
729
  @logged_stdout_filespec =
736
730
  @delegate_object[:logged_stdout_filespec] =
@@ -764,13 +758,14 @@ module MarkdownExec
764
758
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
765
759
  # @param selected [Hash] The selected block.
766
760
  # @return [Array<String>] Required code blocks as an array of lines.
767
- def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new)
761
+ def collect_required_code_lines(mdoc:, selected:, block_source:,
762
+ link_state: LinkState.new)
768
763
  required = mdoc.collect_recursively_required_code(
769
764
  anyname: selected.pub_name,
770
765
  label_format_above: @delegate_object[:shell_code_label_format_above],
771
766
  label_format_below: @delegate_object[:shell_code_label_format_below],
772
767
  block_source: block_source
773
- )
768
+ ) # &bt 'required'
774
769
  dependencies = (link_state&.inherited_dependencies || {}).merge(required[:dependencies] || {})
775
770
  required[:unmet_dependencies] =
776
771
  (required[:unmet_dependencies] || []) - (link_state&.inherited_block_names || [])
@@ -782,18 +777,23 @@ module MarkdownExec
782
777
  runtime_exception(:runtime_exception_error_level,
783
778
  'unmet_dependencies, flag: runtime_exception_error_level',
784
779
  required[:unmet_dependencies])
785
- else
780
+ elsif false ### use option 2024-08-02
786
781
  warn format_and_highlight_dependencies(dependencies,
787
782
  highlight: [@delegate_object[:block_name]])
788
783
  end
789
784
 
790
- code_lines = selected[:shell] == BlockType::VARS ? set_environment_variables_for_block(selected) : []
791
-
792
- HashDelegator.code_merge(link_state&.inherited_lines, required[:code] + code_lines)
785
+ if selected[:shell] == BlockType::OPTS
786
+ # body of blocks is returned as a list of lines to be read an YAML
787
+ HashDelegator.code_merge(required[:blocks].map(&:body).flatten(1))
788
+ else
789
+ code_lines = selected.shell == BlockType::VARS ? set_environment_variables_for_block(selected) : []
790
+ HashDelegator.code_merge(link_state&.inherited_lines,
791
+ required[:code] + code_lines)
792
+ end
793
793
  end
794
794
 
795
795
  def command_execute(command, args: [])
796
- run_state_reset_stream_logs
796
+ @run_state.files = StreamsOut.new
797
797
  @run_state.options = @delegate_object
798
798
  @run_state.started_at = Time.now.utc
799
799
 
@@ -810,7 +810,8 @@ module MarkdownExec
810
810
  else
811
811
  @run_state.in_own_window = false
812
812
  execute_command_with_streams(
813
- [@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args]
813
+ [@delegate_object[:shell], '-c', command,
814
+ @delegate_object[:filename], *args]
814
815
  )
815
816
  end
816
817
 
@@ -820,14 +821,16 @@ module MarkdownExec
820
821
  @run_state.aborted_at = Time.now.utc
821
822
  @run_state.error_message = err.message
822
823
  @run_state.error = err
823
- @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
824
+ @run_state.files.append_stream_line(ExecutionStreams::STD_ERR,
825
+ @run_state.error_message)
824
826
  @fout.fout "Error ENOENT: #{err.inspect}"
825
827
  rescue SignalException => err
826
828
  # Handle SignalException
827
829
  @run_state.aborted_at = Time.now.utc
828
830
  @run_state.error_message = 'SIGTERM'
829
831
  @run_state.error = err
830
- @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
832
+ @run_state.files.append_stream_line(ExecutionStreams::STD_ERR,
833
+ @run_state.error_message)
831
834
  @fout.fout "Error ENOENT: #{err.inspect}"
832
835
  end
833
836
 
@@ -857,18 +860,25 @@ module MarkdownExec
857
860
  # @param mdoc [Object] The markdown document object containing code blocks.
858
861
  # @param selected [Hash] The selected item from the menu to be executed.
859
862
  # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one.
860
- def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:, link_state: nil)
863
+ def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:,
864
+ link_state: nil)
861
865
  required_lines = collect_required_code_lines(mdoc: mdoc, selected: selected, link_state: link_state,
862
866
  block_source: block_source)
863
867
  output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
864
- display_required_code(required_lines: required_lines) if output_or_approval
868
+ if output_or_approval
869
+ display_required_code(required_lines: required_lines)
870
+ end
865
871
  allow_execution = if @delegate_object[:user_must_approve]
866
- prompt_for_user_approval(required_lines: required_lines, selected: selected)
872
+ prompt_for_user_approval(required_lines: required_lines,
873
+ selected: selected)
867
874
  else
868
875
  true
869
876
  end
870
877
 
871
- execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
878
+ if allow_execution
879
+ execute_required_lines(required_lines: required_lines,
880
+ selected: selected)
881
+ end
872
882
 
873
883
  link_state.block_name = nil
874
884
  LoadFileLinkState.new(LoadFile::REUSE, link_state)
@@ -1026,10 +1036,12 @@ module MarkdownExec
1026
1036
  return true unless @delegate_object[:debounce_execution]
1027
1037
 
1028
1038
  # filter block if selected in menu
1029
- return true if @run_state.block_name_from_cli
1039
+ return true if @run_state.source.block_name_from_cli
1030
1040
 
1031
1041
  # return false if @prior_execution_block == @delegate_object[:block_name]
1032
- return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat if @prior_execution_block == @delegate_object[:block_name]
1042
+ if @prior_execution_block == @delegate_object[:block_name]
1043
+ return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat
1044
+ end
1033
1045
 
1034
1046
  @prior_execution_block = @delegate_object[:block_name]
1035
1047
  @allowed_execution_block = nil
@@ -1046,17 +1058,21 @@ module MarkdownExec
1046
1058
  # @param selected_option [Hash] The selected menu option.
1047
1059
  # @return [SelectedBlockMenuState] An object representing the state of the selected block.
1048
1060
  def determine_block_state(selected_option)
1049
- option_name = selected_option.fetch(:oname, nil)
1061
+ option_name = selected_option[:oname]
1050
1062
  if option_name == menu_chrome_formatted_option(:menu_option_exit_name)
1051
1063
  return SelectedBlockMenuState.new(nil,
1064
+ OpenStruct.new,
1052
1065
  MenuState::EXIT)
1053
1066
  end
1054
1067
  if option_name == menu_chrome_formatted_option(:menu_option_back_name)
1055
1068
  return SelectedBlockMenuState.new(selected_option,
1069
+ OpenStruct.new,
1056
1070
  MenuState::BACK)
1057
1071
  end
1058
1072
 
1059
- SelectedBlockMenuState.new(selected_option, MenuState::CONTINUE)
1073
+ SelectedBlockMenuState.new(selected_option,
1074
+ OpenStruct.new,
1075
+ MenuState::CONTINUE)
1060
1076
  end
1061
1077
 
1062
1078
  # Displays the required lines of code with color formatting for the preview section.
@@ -1080,8 +1096,7 @@ module MarkdownExec
1080
1096
  return unless @delegate_object[:save_execution_output]
1081
1097
  return if @run_state.in_own_window
1082
1098
 
1083
- HashDelegator.write_execution_output_to_file(@run_state.files,
1084
- @delegate_object[:logged_stdout_filespec])
1099
+ @run_state.files.write_execution_output_to_file(@delegate_object[:logged_stdout_filespec])
1085
1100
  end
1086
1101
 
1087
1102
  # Select and execute a code block from a Markdown document.
@@ -1096,9 +1111,9 @@ module MarkdownExec
1096
1111
  block_name: @delegate_object[:block_name],
1097
1112
  document_filename: @delegate_object[:filename]
1098
1113
  )
1099
- @run_state.block_name_from_cli = @dml_link_state.block_name.present?
1114
+ @run_state.source.block_name_from_cli = @dml_link_state.block_name.present?
1100
1115
  @cli_block_name = @dml_link_state.block_name
1101
- @dml_now_using_cli = @run_state.block_name_from_cli
1116
+ @dml_now_using_cli = @run_state.source.block_name_from_cli
1102
1117
  @dml_menu_default_dname = nil
1103
1118
  @dml_block_state = SelectedBlockMenuState.new
1104
1119
  @doc_saved_lines_files = []
@@ -1106,51 +1121,98 @@ module MarkdownExec
1106
1121
  ## load file with code lines per options
1107
1122
  #
1108
1123
  if @menu_base_options[:load_code].present?
1109
- @dml_link_state.inherited_lines = []
1110
- @menu_base_options[:load_code].split(':').map do |path|
1111
- @dml_link_state.inherited_lines += File.readlines(path, chomp: true)
1112
- end
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)
1113
1128
 
1114
1129
  inherited_block_names = []
1115
1130
  inherited_dependencies = {}
1116
- selected = { oname: 'load_code' }
1117
- pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
1118
- end
1119
-
1120
- item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
1121
- item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
1122
- item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
1123
- item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
1124
- item_shell = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name]))
1125
- item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
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)
1126
1153
 
1127
1154
  @run_state.batch_random = Random.new.rand
1128
1155
  @run_state.batch_index = 0
1129
1156
 
1157
+ @run_state.files = StreamsOut.new
1158
+
1130
1159
  InputSequencer.new(
1131
1160
  @delegate_object[:filename],
1132
1161
  @delegate_object[:input_cli_rest]
1133
1162
  ).run do |msg, data|
1163
+ # &bt msg
1134
1164
  case msg
1135
1165
  when :parse_document # once for each menu
1136
1166
  # puts "@ - parse document #{data}"
1137
1167
  inpseq_parse_document(data)
1138
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
+
1139
1178
  if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
1140
1179
 
1141
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1180
+ sf = document_name_in_glob_as_file_name(
1181
+ @dml_link_state.document_filename,
1182
+ @delegate_object[:document_saved_lines_glob]
1183
+ )
1142
1184
  files = sf ? Dir.glob(sf) : []
1143
1185
  @doc_saved_lines_files = files.count.positive? ? files : []
1144
1186
 
1145
- lines_count = @dml_link_state.inherited_lines&.count || 0
1187
+ lines_count = @dml_link_state.inherited_lines_count
1146
1188
 
1147
1189
  # add menu items (glob, load, save) and enable selectively
1148
- menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
1149
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD) if files.count.positive?
1150
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT) if lines_count.positive?
1151
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), 1, '', menu_state: MenuState::SAVE) if lines_count.positive?
1152
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), 1, '', menu_state: MenuState::VIEW) if lines_count.positive?
1153
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name])), 1, '', menu_state: MenuState::SHELL) if @delegate_object[:menu_with_shell]
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)
1154
1216
  end
1155
1217
 
1156
1218
  when :display_menu
@@ -1163,7 +1225,7 @@ module MarkdownExec
1163
1225
  if @dml_link_state.block_name.present?
1164
1226
  # @prior_block_was_link = true
1165
1227
  @dml_block_state.block = @dml_blocks_in_file.find do |item|
1166
- item.pub_name == @dml_link_state.block_name
1228
+ item.pub_name == @dml_link_state.block_name || item.oname == @dml_link_state.block_name
1167
1229
  end
1168
1230
  @dml_link_state.block_name = nil
1169
1231
  else
@@ -1176,7 +1238,7 @@ module MarkdownExec
1176
1238
 
1177
1239
  when :execute_block
1178
1240
  case (block_name = data)
1179
- when item_back
1241
+ when item_back.pub_name
1180
1242
  debounce_reset
1181
1243
  @menu_user_clicked_back_link = true
1182
1244
  load_file_link_state = pop_link_history_and_trigger_load
@@ -1191,25 +1253,94 @@ module MarkdownExec
1191
1253
  )
1192
1254
  )
1193
1255
 
1194
- when item_edit
1256
+ when item_edit.pub_name
1195
1257
  debounce_reset
1196
- edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
1258
+ edited = edit_text(@dml_link_state.inherited_lines_block)
1197
1259
  @dml_link_state.inherited_lines = edited.split("\n") if edited
1260
+
1261
+ return :break if pause_user_exit
1262
+
1198
1263
  InputSequencer.next_link_state(prior_block_was_link: true)
1199
1264
 
1200
- when item_load
1265
+ when item_history.pub_name
1201
1266
  debounce_reset
1202
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
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])
1203
1329
  load_filespec = load_filespec_from_expression(sf)
1204
1330
  if load_filespec
1205
- @dml_link_state.inherited_lines ||= []
1206
- @dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
1331
+ @dml_link_state.inherited_lines_append(
1332
+ File.readlines(load_filespec, chomp: true)
1333
+ )
1207
1334
  end
1335
+
1336
+ return :break if pause_user_exit
1337
+
1208
1338
  InputSequencer.next_link_state(prior_block_was_link: true)
1209
1339
 
1210
- when item_save
1340
+ when item_save.pub_name
1211
1341
  debounce_reset
1212
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1342
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
1343
+ @delegate_object[:document_saved_lines_glob])
1213
1344
  save_filespec = save_filespec_from_expression(sf)
1214
1345
  if save_filespec && !write_file_with_directory_creation(
1215
1346
  save_filespec,
@@ -1218,9 +1349,10 @@ module MarkdownExec
1218
1349
  return :break
1219
1350
 
1220
1351
  end
1352
+
1221
1353
  InputSequencer.next_link_state(prior_block_was_link: true)
1222
1354
 
1223
- when item_shell
1355
+ when item_shell.pub_name
1224
1356
  debounce_reset
1225
1357
  loop do
1226
1358
  command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen)
@@ -1236,37 +1368,43 @@ module MarkdownExec
1236
1368
  warn "#{'ERR'.bred} #{exit_status}"
1237
1369
  end
1238
1370
  end
1371
+
1372
+ return :break if pause_user_exit
1373
+
1239
1374
  InputSequencer.next_link_state(prior_block_was_link: true)
1240
1375
 
1241
- when item_view
1376
+ when item_view.pub_name
1242
1377
  debounce_reset
1243
- warn @dml_link_state.inherited_lines.join("\n")
1378
+ warn @dml_link_state.inherited_lines_block
1379
+
1380
+ return :break if pause_user_exit
1381
+
1244
1382
  InputSequencer.next_link_state(prior_block_was_link: true)
1245
1383
 
1246
1384
  else
1247
1385
  @dml_block_state = block_state_for_name_from_cli(block_name)
1248
- if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
1386
+ if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS
1249
1387
  debounce_reset
1250
1388
  link_state = LinkState.new
1251
1389
  options_state = read_show_options_and_trigger_reuse(
1252
- selected: @dml_block_state.block,
1253
- link_state: link_state
1390
+ link_state: link_state,
1391
+ mdoc: @dml_mdoc,
1392
+ selected: @dml_block_state.block
1254
1393
  )
1255
1394
 
1256
- @menu_base_options.merge!(options_state.options)
1257
- @delegate_object.merge!(options_state.options)
1395
+ update_menu_base(options_state.options)
1258
1396
  options_state.load_file_link_state.link_state
1259
1397
  else
1260
1398
  inpseq_execute_block(block_name)
1261
1399
 
1262
- if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1400
+ if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli,
1263
1401
  selected: @dml_block_state.block)
1264
1402
  return :break
1265
1403
  end
1266
1404
 
1267
1405
  ## order of block name processing: link block, cli, from user
1268
1406
  #
1269
- @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1407
+ @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break =
1270
1408
  HashDelegator.next_link_state(
1271
1409
  block_name: @dml_link_state.block_name,
1272
1410
  block_name_from_cli: @dml_now_using_cli,
@@ -1274,14 +1412,14 @@ module MarkdownExec
1274
1412
  was_using_cli: @dml_now_using_cli
1275
1413
  )
1276
1414
 
1277
- if !@dml_block_state.block[:block_name_from_ui] && cli_break
1415
+ if !@dml_block_state.source.block_name_from_ui && cli_break
1278
1416
  # &bsp '!block_name_from_ui + cli_break -> break'
1279
1417
  return :break
1280
1418
  end
1281
1419
 
1282
1420
  InputSequencer.next_link_state(
1283
1421
  block_name: @dml_link_state.block_name,
1284
- prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
1422
+ prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH
1285
1423
  )
1286
1424
  end
1287
1425
  end
@@ -1302,9 +1440,13 @@ module MarkdownExec
1302
1440
  # remove leading "./"
1303
1441
  # replace characters: / : . * (space) with: (underscore)
1304
1442
  def document_name_in_glob_as_file_name(document_filename, glob)
1305
- return document_filename if document_filename.nil? || document_filename.empty?
1443
+ if document_filename.nil? || document_filename.empty?
1444
+ return document_filename
1445
+ end
1306
1446
 
1307
- format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
1447
+ format(glob,
1448
+ { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/,
1449
+ '_') })
1308
1450
  end
1309
1451
 
1310
1452
  def dump_and_warn_block_state(selected:)
@@ -1325,7 +1467,10 @@ module MarkdownExec
1325
1467
  # @param menu_blocks [Hash] Hash of menu blocks.
1326
1468
  # @param link_state [LinkState] Current state of the link.
1327
1469
  def dump_delobj(blocks_in_file, menu_blocks, link_state)
1328
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
1470
+ if @delegate_object[:dump_delegate_object]
1471
+ warn format_and_highlight_hash(@delegate_object,
1472
+ label: '@delegate_object')
1473
+ end
1329
1474
 
1330
1475
  if @delegate_object[:dump_blocks_in_file]
1331
1476
  warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
@@ -1337,11 +1482,18 @@ module MarkdownExec
1337
1482
  label: 'menu_blocks')
1338
1483
  end
1339
1484
 
1340
- warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
1341
- warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
1485
+ if @delegate_object[:dump_inherited_block_names]
1486
+ warn format_and_highlight_lines(link_state.inherited_block_names,
1487
+ label: 'inherited_block_names')
1488
+ end
1489
+ if @delegate_object[:dump_inherited_dependencies]
1490
+ warn format_and_highlight_lines(link_state.inherited_dependencies,
1491
+ label: 'inherited_dependencies')
1492
+ end
1342
1493
  return unless @delegate_object[:dump_inherited_lines]
1343
1494
 
1344
- warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
1495
+ warn format_and_highlight_lines(link_state.inherited_lines,
1496
+ label: 'inherited_lines')
1345
1497
  end
1346
1498
 
1347
1499
  # Opens text in an editor for user modification and returns the modified text.
@@ -1406,7 +1558,7 @@ module MarkdownExec
1406
1558
 
1407
1559
  # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1408
1560
  [lfls.link_state,
1409
- lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
1561
+ lfls.load_file == LoadFile::LOAD ? nil : selected.dname]
1410
1562
  #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1411
1563
  end
1412
1564
 
@@ -1427,17 +1579,20 @@ module MarkdownExec
1427
1579
 
1428
1580
  Open3.popen3(*command) do |stdin, stdout, stderr, exec_thread|
1429
1581
  # Handle stdout stream
1430
- handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
1582
+ handle_stream(stream: stdout,
1583
+ file_type: ExecutionStreams::STD_OUT) do |line|
1431
1584
  yield nil, line, nil, exec_thread if block_given?
1432
1585
  end
1433
1586
 
1434
1587
  # Handle stderr stream
1435
- handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
1588
+ handle_stream(stream: stderr,
1589
+ file_type: ExecutionStreams::STD_ERR) do |line|
1436
1590
  yield nil, nil, line, exec_thread if block_given?
1437
1591
  end
1438
1592
 
1439
1593
  # Handle stdin stream
1440
- input_thread = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
1594
+ input_thread = handle_stream(stream: $stdin,
1595
+ file_type: ExecutionStreams::STD_IN) do |line|
1441
1596
  stdin.puts(line)
1442
1597
  yield line, nil, nil, exec_thread if block_given?
1443
1598
  end
@@ -1464,8 +1619,13 @@ module MarkdownExec
1464
1619
  # @param required_lines [Array<String>] The lines of code to be executed.
1465
1620
  # @param selected [FCB] The selected functional code block object.
1466
1621
  def execute_required_lines(required_lines: [], selected: FCB.new)
1467
- write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script]
1468
- calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state
1622
+ if @delegate_object[:save_executed_script]
1623
+ write_command_file(required_lines: required_lines,
1624
+ selected: selected)
1625
+ end
1626
+ if @dml_block_state
1627
+ calc_logged_stdout_filename(block_name: @dml_block_state.block.oname)
1628
+ end
1469
1629
  format_and_execute_command(code_lines: required_lines)
1470
1630
  post_execution_process
1471
1631
  end
@@ -1479,10 +1639,11 @@ module MarkdownExec
1479
1639
  # @param opts [Hash] Options hash containing configuration settings.
1480
1640
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
1481
1641
  #
1482
- def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new)
1483
- if selected.fetch(:shell, '') == BlockType::LINK
1642
+ def execute_shell_type(selected:, mdoc:, block_source:,
1643
+ link_state: LinkState.new)
1644
+ if selected.shell == BlockType::LINK
1484
1645
  debounce_reset
1485
- push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''),
1646
+ push_link_history_and_trigger_load(link_block_body: selected.body,
1486
1647
  mdoc: mdoc,
1487
1648
  selected: selected,
1488
1649
  link_state: link_state,
@@ -1492,46 +1653,34 @@ module MarkdownExec
1492
1653
  debounce_reset
1493
1654
  pop_link_history_and_trigger_load
1494
1655
 
1495
- elsif selected[:shell] == BlockType::OPTS
1656
+ elsif selected.shell == BlockType::OPTS
1496
1657
  debounce_reset
1497
- block_names = []
1498
1658
  code_lines = []
1499
- dependencies = {}
1500
- options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state)
1501
-
1502
- ## apply options to current state
1503
- #
1504
- @menu_base_options.merge!(options_state.options)
1505
- @delegate_object.merge!(options_state.options)
1659
+ options_state = read_show_options_and_trigger_reuse(
1660
+ link_state: link_state,
1661
+ mdoc: @dml_mdoc,
1662
+ selected: selected
1663
+ )
1664
+ update_menu_base(options_state.options)
1506
1665
 
1507
1666
  ### options_state.load_file_link_state
1508
1667
  link_state = LinkState.new
1509
- link_history_push_and_next(
1510
- curr_block_name: selected.pub_name,
1511
- curr_document_filename: @delegate_object[:filename],
1512
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1513
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1514
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1515
- next_block_name: '',
1516
- next_document_filename: @delegate_object[:filename],
1517
- next_load_file: LoadFile::REUSE
1518
- )
1668
+ next_state_append_code(selected, link_state, code_lines)
1519
1669
 
1520
- elsif selected[:shell] == BlockType::VARS
1670
+ elsif selected.shell == BlockType::PORT
1521
1671
  debounce_reset
1522
- block_names = []
1523
- code_lines = set_environment_variables_for_block(selected)
1524
- dependencies = {}
1525
- link_history_push_and_next(
1526
- curr_block_name: selected.pub_name,
1527
- curr_document_filename: @delegate_object[:filename],
1528
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1529
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1530
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1531
- next_block_name: '',
1532
- next_document_filename: @delegate_object[:filename],
1533
- next_load_file: LoadFile::REUSE
1672
+ required_lines = collect_required_code_lines(
1673
+ mdoc: @dml_mdoc,
1674
+ selected: selected,
1675
+ link_state: link_state,
1676
+ block_source: block_source
1534
1677
  )
1678
+ next_state_set_code(selected, link_state, required_lines)
1679
+
1680
+ elsif selected.shell == BlockType::VARS
1681
+ debounce_reset
1682
+ next_state_append_code(selected, link_state,
1683
+ set_environment_variables_for_block(selected))
1535
1684
 
1536
1685
  elsif debounce_allows
1537
1686
  compile_execute_and_trigger_reuse(mdoc: mdoc,
@@ -1558,6 +1707,21 @@ module MarkdownExec
1558
1707
  string_send_color(data_string, color_sym)
1559
1708
  end
1560
1709
 
1710
+ # size of a file in bytes and the number of lines
1711
+ def file_info(file_path)
1712
+ file_size = 0
1713
+ line_count = 0
1714
+
1715
+ File.open(file_path, 'r') do |file|
1716
+ file.each_line do |_line|
1717
+ line_count += 1
1718
+ end
1719
+ file_size = file.size
1720
+ end
1721
+
1722
+ { size: file_size, lines: line_count }
1723
+ end
1724
+
1561
1725
  def format_and_execute_command(code_lines:)
1562
1726
  formatted_command = code_lines.flatten.join("\n")
1563
1727
  @fout.fout fetch_color(data_sym: :script_execution_head,
@@ -1605,6 +1769,16 @@ module MarkdownExec
1605
1769
  expr.include?('%{') ? format_expression(expr) : expr
1606
1770
  end
1607
1771
 
1772
+ def generate_temp_filename(ext = '.sh')
1773
+ filename = begin
1774
+ Dir::Tmpname.make_tmpname(['x', ext], nil)
1775
+ rescue NoMethodError
1776
+ require 'securerandom'
1777
+ "#{SecureRandom.urlsafe_base64}#{ext}"
1778
+ end
1779
+ File.join(Dir.tmpdir, filename)
1780
+ end
1781
+
1608
1782
  # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1609
1783
  # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1610
1784
  #
@@ -1618,7 +1792,7 @@ module MarkdownExec
1618
1792
  bm = extract_named_captures_from_option(titlexcall,
1619
1793
  @delegate_object[:block_name_match])
1620
1794
 
1621
- shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
1795
+ shell_color_option = SHELL_COLOR_OPTIONS[fcb.shell]
1622
1796
 
1623
1797
  if @delegate_object[:block_name_nick_match].present? && fcb.oname =~ Regexp.new(@delegate_object[:block_name_nick_match])
1624
1798
  fcb.nickname = $~[0]
@@ -1629,9 +1803,10 @@ module MarkdownExec
1629
1803
 
1630
1804
  fcb.dname = HashDelegator.indent_all_lines(
1631
1805
  apply_shell_color_option(fcb.oname, shell_color_option),
1632
- fcb.fetch(:indent, nil)
1806
+ fcb.indent
1633
1807
  )
1634
- fcb
1808
+
1809
+ fcb # &br
1635
1810
  end
1636
1811
 
1637
1812
  # Updates the delegate object's state based on the provided block state.
@@ -1654,13 +1829,13 @@ module MarkdownExec
1654
1829
  Thread.new do
1655
1830
  stream.each_line do |line|
1656
1831
  line.strip!
1657
- @run_state.files[file_type] << line if @run_state.files
1658
-
1659
- if @delegate_object[:output_stdout]
1660
- # print line
1661
- puts line
1832
+ if @run_state.files.streams
1833
+ @run_state.files.append_stream_line(file_type,
1834
+ line)
1662
1835
  end
1663
1836
 
1837
+ puts line if @delegate_object[:output_stdout]
1838
+
1664
1839
  yield line if block_given?
1665
1840
  end
1666
1841
  rescue IOError
@@ -1671,15 +1846,40 @@ module MarkdownExec
1671
1846
  end
1672
1847
  end
1673
1848
 
1849
+ def history_files(link_state, order: :chronological, direction: :reverse)
1850
+ files = Dir.glob(
1851
+ File.join(
1852
+ @delegate_object[:saved_script_folder],
1853
+ SavedAsset.new(
1854
+ filename: @delegate_object[:filename],
1855
+ saved_asset_format: shell_escape_asset_format(link_state)
1856
+ ).generate_name
1857
+ )
1858
+ )
1859
+
1860
+ sorted_files = case order
1861
+ when :alphabetical
1862
+ files.sort
1863
+ when :chronological
1864
+ files.sort_by { |file| File.mtime(file) }
1865
+ else
1866
+ raise ArgumentError, "Invalid order: #{order}"
1867
+ end
1868
+
1869
+ direction == :reverse ? sorted_files.reverse : sorted_files
1870
+ end
1871
+
1674
1872
  # Initializes variables for regex and other states
1675
1873
  def initial_state
1676
1874
  {
1677
- fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch(
1678
- :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1679
- )),
1680
- fenced_start_extended_regex: Regexp.new(@delegate_object.fetch(
1681
- :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1682
- )),
1875
+ fenced_start_and_end_regex:
1876
+ Regexp.new(@delegate_object.fetch(
1877
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1878
+ )),
1879
+ fenced_start_extended_regex:
1880
+ Regexp.new(@delegate_object.fetch(
1881
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1882
+ )),
1683
1883
  fcb: MarkdownExec::FCB.new,
1684
1884
  in_fenced_block: false,
1685
1885
  headings: []
@@ -1689,7 +1889,7 @@ module MarkdownExec
1689
1889
  def inpseq_execute_block(block_name)
1690
1890
  @dml_block_state = block_state_for_name_from_cli(block_name)
1691
1891
  dump_and_warn_block_state(selected: @dml_block_state.block)
1692
- @dml_link_state, @dml_menu_default_dname = \
1892
+ @dml_link_state, @dml_menu_default_dname =
1693
1893
  exec_bash_next_state(
1694
1894
  selected: @dml_block_state.block,
1695
1895
  mdoc: @dml_mdoc,
@@ -1706,8 +1906,8 @@ module MarkdownExec
1706
1906
  @run_state.in_own_window = false
1707
1907
 
1708
1908
  # &bsp 'loop', block_name_from_cli, @cli_block_name
1709
- @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
1710
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
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,
1711
1911
  now_using_cli: @dml_now_using_cli,
1712
1912
  link_state: @dml_link_state)
1713
1913
  end
@@ -1716,7 +1916,7 @@ module MarkdownExec
1716
1916
  @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1717
1917
  menu_blocks: @dml_menu_blocks,
1718
1918
  default: @dml_menu_default_dname)
1719
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1919
+ # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
1720
1920
  if !@dml_block_state
1721
1921
  HashDelegator.error_handler('block_state missing', { abort: true })
1722
1922
  elsif @dml_block_state.state == MenuState::EXIT
@@ -1742,8 +1942,10 @@ module MarkdownExec
1742
1942
  end
1743
1943
  end
1744
1944
 
1745
- def link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source:)
1746
- all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines)
1945
+ def link_block_data_eval(link_state, code_lines, selected, link_block_data,
1946
+ block_source:)
1947
+ all_code = HashDelegator.code_merge(link_state&.inherited_lines,
1948
+ code_lines)
1747
1949
  output_lines = []
1748
1950
 
1749
1951
  Tempfile.open do |file|
@@ -1752,7 +1954,7 @@ module MarkdownExec
1752
1954
  file.rewind
1753
1955
 
1754
1956
  if link_block_data.fetch(LinkKeys::EXEC, false)
1755
- run_state_reset_stream_logs
1957
+ @run_state.files = StreamsOut.new
1756
1958
  execute_command_with_streams([cmd]) do |_stdin, stdout, stderr, _thread|
1757
1959
  line = stdout || stderr
1758
1960
  output_lines.push(line) if line
@@ -1762,7 +1964,8 @@ module MarkdownExec
1762
1964
  #
1763
1965
  output_lines = process_string_array(
1764
1966
  output_lines,
1765
- begin_pattern: @delegate_object.fetch(:output_assignment_begin, nil),
1967
+ begin_pattern: @delegate_object.fetch(:output_assignment_begin,
1968
+ nil),
1766
1969
  end_pattern: @delegate_object.fetch(:output_assignment_end, nil),
1767
1970
  scan1: @delegate_object.fetch(:output_assignment_match, nil),
1768
1971
  format1: @delegate_object.fetch(:output_assignment_format, nil)
@@ -1773,7 +1976,10 @@ module MarkdownExec
1773
1976
  end
1774
1977
  end
1775
1978
 
1776
- HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true }) unless output_lines
1979
+ unless output_lines
1980
+ HashDelegator.error_handler('all_code eval output_lines is nil',
1981
+ { abort: true })
1982
+ end
1777
1983
 
1778
1984
  label_format_above = @delegate_object[:shell_code_label_format_above]
1779
1985
  label_format_below = @delegate_object[:shell_code_label_format_below]
@@ -1782,7 +1988,11 @@ module MarkdownExec
1782
1988
  block_source.merge({ block_name: selected.pub_name }))] +
1783
1989
  output_lines.map do |line|
1784
1990
  re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
1785
- re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line
1991
+ next unless re =~ line
1992
+
1993
+ re.gsub_format(line,
1994
+ link_block_data.fetch('format',
1995
+ '%{line}'))
1786
1996
  end.compact +
1787
1997
  [label_format_below && format(label_format_below,
1788
1998
  block_source.merge({ block_name: selected.pub_name }))]
@@ -1831,34 +2041,41 @@ module MarkdownExec
1831
2041
  # Executes a specified block once per filename.
1832
2042
  # @param all_blocks [Array] Array of all block elements.
1833
2043
  # @return [Boolean, nil] True if values were modified, nil otherwise.
1834
- def load_auto_opts_block(all_blocks)
2044
+ def load_auto_opts_block(all_blocks, mdoc:)
1835
2045
  block_name = @delegate_object[:document_load_opts_block_name]
1836
- return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
2046
+ unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
2047
+ return
2048
+ end
1837
2049
 
1838
2050
  block = HashDelegator.block_find(all_blocks, :oname, block_name)
1839
2051
  return unless block
1840
2052
 
1841
- options_state = read_show_options_and_trigger_reuse(selected: block)
1842
- @menu_base_options.merge!(options_state.options)
1843
- @delegate_object.merge!(options_state.options)
2053
+ options_state = read_show_options_and_trigger_reuse(
2054
+ mdoc: mdoc,
2055
+ selected: block
2056
+ )
2057
+ update_menu_base(options_state.options)
1844
2058
 
1845
2059
  @most_recent_loaded_filename = @delegate_object[:filename]
1846
2060
  true
1847
2061
  end
1848
2062
 
1849
- def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
2063
+ def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [],
2064
+ default: nil)
1850
2065
  if @delegate_object[:block_name].present?
1851
2066
  block = all_blocks.find do |item|
1852
2067
  item.pub_name == @delegate_object[:block_name]
1853
- end&.merge(block_name_from_ui: false)
2068
+ end
2069
+ source = OpenStruct.new(block_name_from_ui: false)
1854
2070
  else
1855
2071
  block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
1856
2072
  default)
1857
- block = block_state.block&.merge(block_name_from_ui: true)
2073
+ block = block_state.block
2074
+ source = OpenStruct.new(block_name_from_ui: true)
1858
2075
  state = block_state.state
1859
2076
  end
1860
2077
 
1861
- SelectedBlockMenuState.new(block, state)
2078
+ SelectedBlockMenuState.new(block, source, state)
1862
2079
  rescue StandardError
1863
2080
  HashDelegator.error_handler('load_cli_or_user_selected_block')
1864
2081
  end
@@ -1876,17 +2093,23 @@ module MarkdownExec
1876
2093
  expanded_expression
1877
2094
  end
1878
2095
  end
2096
+
1879
2097
  # Handle expression with wildcard characters
1880
2098
  def load_filespec_wildcard_expansion(expr, auto_load_single: false)
1881
2099
  files = find_files(expr)
1882
2100
  if files.count.zero?
1883
- HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
2101
+ HashDelegator.error_handler("no files found with '#{expr}' ",
2102
+ { abort: true })
1884
2103
  elsif auto_load_single && files.count == 1
1885
2104
  files.first
1886
2105
  else
1887
2106
  ## user selects from existing files or other
1888
2107
  #
1889
- case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
2108
+ case (name = prompt_select_code_filename(
2109
+ [@delegate_object[:prompt_filespec_back]] + files,
2110
+ string: @delegate_object[:prompt_select_code_file],
2111
+ color_sym: :prompt_color_after_script_execution
2112
+ ))
1890
2113
  when @delegate_object[:prompt_filespec_back]
1891
2114
  # do nothing
1892
2115
  else
@@ -1910,20 +2133,22 @@ module MarkdownExec
1910
2133
 
1911
2134
  # recreate menu with new options
1912
2135
  #
1913
- all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
2136
+ all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(
2137
+ all_blocks, mdoc: mdoc
2138
+ )
1914
2139
 
1915
2140
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1916
2141
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
1917
2142
  ### compress empty lines
1918
2143
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
1919
- [all_blocks, menu_blocks, mdoc]
2144
+ [all_blocks, menu_blocks, mdoc] # &br
1920
2145
  end
1921
2146
 
1922
2147
  def menu_add_disabled_option(name)
1923
2148
  raise unless name.present?
1924
2149
  raise if @dml_menu_blocks.nil?
1925
2150
 
1926
- block = @dml_menu_blocks.find { |item| item[:oname] == name }
2151
+ block = @dml_menu_blocks.find { |item| item.oname == name }
1927
2152
 
1928
2153
  # create menu item when it is needed (count > 0)
1929
2154
  #
@@ -1961,7 +2186,9 @@ module MarkdownExec
1961
2186
  # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
1962
2187
  # @return [String] The formatted or original value of the menu option.
1963
2188
  def menu_chrome_formatted_option(option_symbol = :menu_option_back_name)
1964
- option_value = HashDelegator.safeval(@delegate_object.fetch(option_symbol, ''))
2189
+ option_value = HashDelegator.safeval(@delegate_object.fetch(
2190
+ option_symbol, ''
2191
+ ))
1965
2192
 
1966
2193
  if @delegate_object[:menu_chrome_format]
1967
2194
  format(@delegate_object[:menu_chrome_format], option_value)
@@ -1974,20 +2201,20 @@ module MarkdownExec
1974
2201
  raise unless name.present?
1975
2202
  raise if @dml_menu_blocks.nil?
1976
2203
 
1977
- item = @dml_menu_blocks.find { |block| block[:oname] == name }
2204
+ item = @dml_menu_blocks.find { |block| block.oname == name }
1978
2205
 
1979
2206
  # create menu item when it is needed (count > 0)
1980
2207
  #
1981
2208
  if item.nil? && count.positive?
1982
- append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
1983
- item = @dml_menu_blocks.find { |block| block[:oname] == name }
2209
+ item = append_chrome_block(menu_blocks: @dml_menu_blocks,
2210
+ menu_state: menu_state)
1984
2211
  end
1985
2212
 
1986
2213
  # update item if it exists
1987
2214
  #
1988
2215
  return unless item
1989
2216
 
1990
- item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name
2217
+ item.dname = type.present? ? "#{name} (#{count} #{type})" : name
1991
2218
  if count.positive?
1992
2219
  item.delete(:disabled)
1993
2220
  else
@@ -1995,14 +2222,15 @@ module MarkdownExec
1995
2222
  end
1996
2223
  end
1997
2224
 
1998
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
2225
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:,
2226
+ link_state:)
1999
2227
  if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2000
2228
  # &bsp 'pause cli control, allow user to select block'
2001
2229
  block_name_from_cli = false
2002
2230
  now_using_cli = false
2003
- @menu_base_options[:block_name] = \
2231
+ @menu_base_options[:block_name] =
2004
2232
  @delegate_object[:block_name] = \
2005
- link_state.block_name = \
2233
+ link_state.block_name =
2006
2234
  @cli_block_name = nil
2007
2235
  end
2008
2236
 
@@ -2023,6 +2251,27 @@ module MarkdownExec
2023
2251
  end
2024
2252
  end
2025
2253
 
2254
+ def next_state_append_code(selected, link_state, code_lines)
2255
+ next_state_set_code(selected, link_state, HashDelegator.code_merge(
2256
+ link_state&.inherited_lines, code_lines
2257
+ ))
2258
+ end
2259
+
2260
+ def next_state_set_code(selected, link_state, code_lines)
2261
+ block_names = []
2262
+ dependencies = {}
2263
+ link_history_push_and_next(
2264
+ curr_block_name: selected.pub_name,
2265
+ curr_document_filename: @delegate_object[:filename],
2266
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2267
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2268
+ inherited_lines: HashDelegator.code_merge(code_lines),
2269
+ next_block_name: '',
2270
+ next_document_filename: @delegate_object[:filename],
2271
+ next_load_file: LoadFile::REUSE
2272
+ )
2273
+ end
2274
+
2026
2275
  def output_color_formatted(data_sym, color_sym)
2027
2276
  formatted_string = string_send_color(@delegate_object[data_sym],
2028
2277
  color_sym)
@@ -2053,16 +2302,16 @@ module MarkdownExec
2053
2302
  def output_execution_summary
2054
2303
  return unless @delegate_object[:output_execution_summary]
2055
2304
 
2056
- fout_section 'summary', {
2305
+ @fout.fout_section 'summary', {
2057
2306
  execute_aborted_at: @run_state.aborted_at,
2058
2307
  execute_completed_at: @run_state.completed_at,
2059
2308
  execute_error: @run_state.error,
2060
2309
  execute_error_message: @run_state.error_message,
2061
- execute_files: @run_state.files,
2062
2310
  execute_options: @run_state.options,
2063
2311
  execute_started_at: @run_state.started_at,
2312
+ saved_filespec: @run_state.saved_filespec,
2064
2313
  script_block_name: @run_state.script_block_name,
2065
- saved_filespec: @run_state.saved_filespec
2314
+ streamed_lines: @run_state.files.streams
2066
2315
  }
2067
2316
  end
2068
2317
 
@@ -2075,6 +2324,11 @@ module MarkdownExec
2075
2324
  ), level: level
2076
2325
  end
2077
2326
 
2327
+ def pause_user_exit
2328
+ @delegate_object[:pause_after_script_execution] &&
2329
+ prompt_select_continue == MenuState::EXIT
2330
+ end
2331
+
2078
2332
  def pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
2079
2333
  dependencies, selected, next_block_name: nil)
2080
2334
  pop = @link_history.pop # updatable
@@ -2098,9 +2352,12 @@ module MarkdownExec
2098
2352
  link_history_push_and_next(
2099
2353
  curr_block_name: selected.pub_name,
2100
2354
  curr_document_filename: @delegate_object[:filename],
2101
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2102
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2103
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2355
+ inherited_block_names:
2356
+ ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2357
+ inherited_dependencies:
2358
+ (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2359
+ inherited_lines:
2360
+ HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2104
2361
  next_block_name: next_block_name,
2105
2362
  next_document_filename: @delegate_object[:filename], # not next_document_filename
2106
2363
  next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
@@ -2116,12 +2373,15 @@ module MarkdownExec
2116
2373
  def pop_link_history_and_trigger_load
2117
2374
  pop = @link_history.pop
2118
2375
  peek = @link_history.peek
2119
- LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
2120
- document_filename: pop.document_filename,
2121
- inherited_block_names: peek.inherited_block_names,
2122
- inherited_dependencies: peek.inherited_dependencies,
2123
- inherited_lines: peek.inherited_lines
2124
- ))
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
+ )
2384
+ )
2125
2385
  end
2126
2386
 
2127
2387
  def post_execution_process
@@ -2137,20 +2397,21 @@ module MarkdownExec
2137
2397
  # @return [Array<Hash>] The updated blocks menu.
2138
2398
  def prepare_blocks_menu(menu_blocks)
2139
2399
  menu_blocks.map do |fcb|
2140
- next if Filter.prepared_not_in_menu?(@delegate_object, fcb,
2141
- %i[block_name_include_match block_name_wrapper_match])
2400
+ next if Filter.prepared_not_in_menu?(
2401
+ @delegate_object,
2402
+ fcb,
2403
+ %i[block_name_include_match block_name_wrapper_match]
2404
+ )
2142
2405
 
2143
- fcb.merge!(
2144
- name: fcb.dname,
2145
- label: BlockLabel.make(
2146
- body: fcb[:body],
2147
- filename: @delegate_object[:filename],
2148
- headings: fcb.fetch(:headings, []),
2149
- menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname],
2150
- menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings],
2151
- text: fcb[:text],
2152
- title: fcb[:title]
2153
- )
2406
+ fcb.name = fcb.dname
2407
+ fcb.label = BlockLabel.make(
2408
+ body: fcb.body,
2409
+ filename: @delegate_object[:filename],
2410
+ headings: fcb.headings,
2411
+ menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname],
2412
+ menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings],
2413
+ text: fcb.text,
2414
+ title: fcb.title
2154
2415
  )
2155
2416
  fcb.to_h
2156
2417
  end.compact
@@ -2171,7 +2432,10 @@ module MarkdownExec
2171
2432
  when :filter
2172
2433
  %i[blocks line]
2173
2434
  when :line
2174
- create_and_add_chrome_blocks(blocks, fcb) unless @delegate_object[:no_chrome]
2435
+ unless @delegate_object[:no_chrome]
2436
+ create_and_add_chrome_blocks(blocks,
2437
+ fcb)
2438
+ end
2175
2439
  end
2176
2440
  end
2177
2441
 
@@ -2244,7 +2508,8 @@ module MarkdownExec
2244
2508
  # @param filespec [String] the wildcard expression to be substituted
2245
2509
  # @return [String, nil] the resolved path or substituted expression, or nil if interrupted
2246
2510
  def prompt_for_filespec_with_wildcard(filespec)
2247
- puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
2511
+ puts format(@delegate_object[:prompt_show_expr_format],
2512
+ { expr: filespec })
2248
2513
  puts @delegate_object[:prompt_enter_filespec]
2249
2514
 
2250
2515
  begin
@@ -2301,27 +2566,40 @@ module MarkdownExec
2301
2566
 
2302
2567
  # public
2303
2568
 
2304
- def prompt_select_code_filename(filenames)
2569
+ def prompt_select_code_filename(
2570
+ filenames,
2571
+ color_sym: :prompt_color_after_script_execution,
2572
+ cycle: true,
2573
+ enum: false,
2574
+ quiet: true,
2575
+ string: @delegate_object[:prompt_select_code_file]
2576
+ )
2305
2577
  @prompt.select(
2306
- string_send_color(@delegate_object[:prompt_select_code_file],
2307
- :prompt_color_after_script_execution),
2308
- filter: true,
2309
- quiet: true
2578
+ string_send_color(string, color_sym),
2579
+ cycle: cycle,
2580
+ filter: !enum,
2581
+ per_page: @delegate_object[:select_page_height],
2582
+ quiet: quiet
2310
2583
  ) do |menu|
2311
- filenames.each do |filename|
2312
- menu.choice filename
2584
+ menu.enum '.' if enum
2585
+ filenames.each.with_index do |filename, ind|
2586
+ if enum
2587
+ menu.choice filename, ind + 1
2588
+ else
2589
+ menu.choice filename
2590
+ end
2313
2591
  end
2314
2592
  end
2315
2593
  rescue TTY::Reader::InputInterrupt
2316
2594
  exit 1
2317
2595
  end
2318
2596
 
2319
- def prompt_select_continue
2597
+ def prompt_select_continue(filter: true, quiet: true)
2320
2598
  sel = @prompt.select(
2321
2599
  string_send_color(@delegate_object[:prompt_after_script_execution],
2322
2600
  :prompt_color_after_script_execution),
2323
- filter: true,
2324
- quiet: true
2601
+ filter: filter,
2602
+ quiet: quiet
2325
2603
  ) do |menu|
2326
2604
  menu.choice @delegate_object[:prompt_yes]
2327
2605
  menu.choice @delegate_object[:prompt_exit]
@@ -2334,7 +2612,7 @@ module MarkdownExec
2334
2612
  # user prompt to exit if the menu will be displayed again
2335
2613
  #
2336
2614
  def prompt_user_exit(block_name_from_cli:, selected:)
2337
- selected[:shell] == BlockType::BASH &&
2615
+ selected.shell == BlockType::BASH &&
2338
2616
  @delegate_object[:pause_after_script_execution] &&
2339
2617
  prompt_select_continue == MenuState::EXIT
2340
2618
  end
@@ -2383,18 +2661,26 @@ module MarkdownExec
2383
2661
  #
2384
2662
  if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
2385
2663
  load_filespec = load_filespec_from_expression(load_expr)
2386
- code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
2664
+ if load_filespec
2665
+ code_lines += File.readlines(load_filespec,
2666
+ chomp: true)
2667
+ end
2387
2668
  end
2388
2669
 
2389
2670
  # if an eval link block, evaluate code_lines and return its standard output
2390
2671
  #
2391
2672
  if link_block_data.fetch(LinkKeys::EVAL,
2392
- false) || link_block_data.fetch(LinkKeys::EXEC, false)
2393
- code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
2673
+ false) || link_block_data.fetch(LinkKeys::EXEC,
2674
+ false)
2675
+ code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data,
2676
+ block_source: block_source)
2394
2677
  end
2395
2678
 
2396
- next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
2397
- next_block_name = link_block_data.fetch(LinkKeys::NEXT_BLOCK, nil) || link_block_data.fetch(LinkKeys::BLOCK, nil) || ''
2679
+ next_document_filename = write_inherited_lines_to_file(link_state,
2680
+ link_block_data)
2681
+ next_block_name = link_block_data.fetch(LinkKeys::NEXT_BLOCK,
2682
+ nil) || link_block_data.fetch(LinkKeys::BLOCK,
2683
+ nil) || ''
2398
2684
 
2399
2685
  if link_block_data[LinkKeys::RETURN]
2400
2686
  pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
@@ -2406,7 +2692,9 @@ module MarkdownExec
2406
2692
  curr_document_filename: @delegate_object[:filename],
2407
2693
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2408
2694
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2409
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2695
+ inherited_lines: HashDelegator.code_merge(
2696
+ link_state&.inherited_lines, code_lines
2697
+ ),
2410
2698
  next_block_name: next_block_name,
2411
2699
  next_document_filename: next_document_filename,
2412
2700
  next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
@@ -2427,14 +2715,29 @@ module MarkdownExec
2427
2715
  # @param selected [Hash] Selected item from the menu containing a YAML body.
2428
2716
  # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2429
2717
  # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2430
- def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2718
+ def read_show_options_and_trigger_reuse(selected:,
2719
+ mdoc:, link_state: LinkState.new)
2431
2720
  obj = {}
2432
- data = YAML.load(selected[:body].join("\n"))
2433
- (data || []).each do |key, value|
2434
- sym_key = key.to_sym
2435
- obj[sym_key] = value
2436
2721
 
2437
- print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2722
+ # concatenated body of all required blocks loaded a YAML
2723
+ data = YAML.load(
2724
+ collect_required_code_lines(
2725
+ mdoc: mdoc, selected: selected,
2726
+ link_state: link_state, block_source: {}
2727
+ ).join("\n")
2728
+ ).transform_keys(&:to_sym)
2729
+
2730
+ if selected.shell == BlockType::OPTS
2731
+ obj = data
2732
+ else
2733
+ (data || []).each do |key, value|
2734
+ sym_key = key.to_sym
2735
+ obj[sym_key] = value
2736
+
2737
+ if @delegate_object[:menu_opts_set_format].present?
2738
+ print_formatted_option(key, value)
2739
+ end
2740
+ end
2438
2741
  end
2439
2742
 
2440
2743
  link_state.block_name = nil
@@ -2445,7 +2748,7 @@ module MarkdownExec
2445
2748
  end
2446
2749
 
2447
2750
  # Registers console attributes by modifying the options hash.
2448
- # This method handles terminal resizing and adjusts the console dimensions
2751
+ # This method handles terminal resizing and adjusts the console dimensions
2449
2752
  # and pagination settings based on the current terminal size.
2450
2753
  #
2451
2754
  # @param opts [Hash] a hash containing various options for the console settings.
@@ -2462,19 +2765,23 @@ module MarkdownExec
2462
2765
  # register_console_attributes(opts)
2463
2766
  # # opts will be updated with the current console dimensions and pagination settings.
2464
2767
  def register_console_attributes(opts)
2465
- begin
2466
- if (resized = @delegate_object[:menu_resize_terminal])
2467
- resize_terminal
2468
- end
2768
+ if (resized = @delegate_object[:menu_resize_terminal])
2769
+ resize_terminal
2770
+ end
2469
2771
 
2470
- if resized || !opts[:console_width]
2471
- opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize
2472
- end
2772
+ if resized || !opts[:console_width]
2773
+ opts[:console_height], opts[:console_width] = opts[:console_winsize] =
2774
+ IO.console.winsize
2775
+ end
2473
2776
 
2474
- opts[:per_page] = opts[:select_page_height] = [opts[:console_height] - 3, 4].max unless opts[:select_page_height]&.positive?
2475
- rescue StandardError
2476
- HashDelegator.error_handler('register_console_attributes', { abort: true })
2777
+ unless opts[:select_page_height]&.positive?
2778
+ opts[:per_page] =
2779
+ opts[:select_page_height] =
2780
+ [opts[:console_height] - 3, 4].max
2477
2781
  end
2782
+ rescue StandardError
2783
+ HashDelegator.error_handler('register_console_attributes',
2784
+ { abort: true })
2478
2785
  end
2479
2786
 
2480
2787
  # Check if the delegate object responds to a given method.
@@ -2486,20 +2793,14 @@ module MarkdownExec
2486
2793
  true
2487
2794
  elsif @delegate_object.respond_to?(method_name, include_private)
2488
2795
  true
2489
- elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
2796
+ elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=,
2797
+ include_private)
2490
2798
  true
2491
2799
  else
2492
2800
  @delegate_object.respond_to?(method_name, include_private)
2493
2801
  end
2494
2802
  end
2495
2803
 
2496
- def run_state_reset_stream_logs
2497
- @run_state.files = Hash.new()
2498
- @run_state.files[ExecutionStreams::STD_ERR] = []
2499
- @run_state.files[ExecutionStreams::STD_IN] = []
2500
- @run_state.files[ExecutionStreams::STD_OUT] = []
2501
- end
2502
-
2503
2804
  def runtime_exception(exception_sym, name, items)
2504
2805
  if @delegate_object[exception_sym] != 0
2505
2806
  data = { name: name, detail: items.join(', ') }
@@ -2543,8 +2844,12 @@ module MarkdownExec
2543
2844
  ## user selects from existing files or other
2544
2845
  # input into path with wildcard for easy entry
2545
2846
  #
2546
- name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
2547
- case name
2847
+ case (name = prompt_select_code_filename(
2848
+ [@delegate_object[:prompt_filespec_back],
2849
+ @delegate_object[:prompt_filespec_other]] + files,
2850
+ string: @delegate_object[:prompt_select_code_file],
2851
+ color_sym: :prompt_color_after_script_execution
2852
+ ))
2548
2853
  when @delegate_object[:prompt_filespec_back]
2549
2854
  # do nothing
2550
2855
  when @delegate_object[:prompt_filespec_other]
@@ -2561,37 +2866,46 @@ module MarkdownExec
2561
2866
  end
2562
2867
 
2563
2868
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
2564
- def select_option_with_metadata(prompt_text, names, opts = {})
2869
+ def select_option_with_metadata(prompt_text, menu_items, opts = {})
2565
2870
  ## configure to environment
2566
2871
  #
2567
2872
  register_console_attributes(opts)
2568
2873
 
2569
2874
  # crashes if all menu options are disabled
2570
2875
  selection = @prompt.select(prompt_text,
2571
- names,
2876
+ menu_items,
2572
2877
  opts.merge(filter: true))
2573
- selected_name = names.find do |item|
2878
+
2879
+ selected = menu_items.find do |item|
2574
2880
  if item.instance_of?(Hash)
2575
- item[:dname] == selection
2881
+ (item[:name] || item[:dname]) == selection
2882
+ elsif item.instance_of?(MarkdownExec::FCB)
2883
+ item.dname == selection
2576
2884
  else
2577
2885
  item == selection
2578
2886
  end
2579
2887
  end
2580
- selected_name = { dname: selected_name } if selected_name.instance_of?(String)
2581
- unless selected_name
2582
- HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
2888
+ if selected.instance_of?(String)
2889
+ selected = FCB.new(dname: selected)
2890
+ elsif selected.instance_of?(Hash)
2891
+ selected = FCB.new(selected)
2892
+ end
2893
+ unless selected
2894
+ HashDelegator.error_handler('select_option_with_metadata',
2895
+ error: 'menu item not found')
2583
2896
  exit 1
2584
2897
  end
2585
2898
 
2586
- selected_name.merge(
2587
- if selection == menu_chrome_colored_option(:menu_option_back_name)
2588
- { option: selection, shell: BlockType::LINK }
2589
- elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
2590
- { option: selection }
2591
- else
2592
- { selected: selection }
2593
- end
2594
- )
2899
+ if selection == menu_chrome_colored_option(:menu_option_back_name)
2900
+ selected.option = selection
2901
+ selected.shell = BlockType::LINK
2902
+ elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
2903
+ selected.option = selection
2904
+ else
2905
+ selected.selected = selection
2906
+ end
2907
+
2908
+ selected
2595
2909
  rescue TTY::Reader::InputInterrupt
2596
2910
  exit 1
2597
2911
  rescue StandardError
@@ -2611,8 +2925,9 @@ module MarkdownExec
2611
2925
  block_name_from_cli ? @cli_block_name : link_state.block_name
2612
2926
  end
2613
2927
 
2614
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2615
- block_name_from_cli, now_using_cli = \
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 =
2616
2931
  manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2617
2932
  now_using_cli: now_using_cli,
2618
2933
  link_state: link_state)
@@ -2629,7 +2944,7 @@ module MarkdownExec
2629
2944
 
2630
2945
  def set_environment_variables_for_block(selected)
2631
2946
  code_lines = []
2632
- YAML.load(selected[:body].join("\n"))&.each do |key, value|
2947
+ YAML.load(selected.body.join("\n"))&.each do |key, value|
2633
2948
  ENV[key] = value.to_s
2634
2949
 
2635
2950
  require 'shellwords'
@@ -2644,6 +2959,29 @@ module MarkdownExec
2644
2959
  code_lines
2645
2960
  end
2646
2961
 
2962
+ def shell_escape_asset_format(link_state)
2963
+ raw = @delegate_object[:saved_asset_format]
2964
+
2965
+ return raw unless @delegate_object[:shell_parameter_expansion]
2966
+
2967
+ # unchanged if no parameter expansion takes place
2968
+ return raw unless /$/ =~ raw
2969
+
2970
+ filespec = generate_temp_filename
2971
+ cmd = [@delegate_object[:shell], '-c', filespec].join(' ')
2972
+
2973
+ marker = Random.new.rand.to_s
2974
+
2975
+ code = (link_state&.inherited_lines || []) + ["echo -n \"#{marker}#{raw}\""]
2976
+ # &bt code
2977
+ File.write filespec, HashDelegator.join_code_lines(code)
2978
+ File.chmod 0o755, filespec
2979
+
2980
+ out = `#{cmd}`.sub(/.*?#{marker}/m, '')
2981
+ File.delete filespec
2982
+ out # &br
2983
+ end
2984
+
2647
2985
  def should_add_back_option?
2648
2986
  @delegate_object[:menu_with_back] && @link_history.prior_state_exist?
2649
2987
  end
@@ -2766,6 +3104,13 @@ module MarkdownExec
2766
3104
  end
2767
3105
  end
2768
3106
 
3107
+ ## apply options to current state
3108
+ #
3109
+ def update_menu_base(options)
3110
+ @menu_base_options.merge!(options)
3111
+ @delegate_object.merge!(options)
3112
+ end
3113
+
2769
3114
  def wait_for_stream_processing
2770
3115
  @process_mutex.synchronize do
2771
3116
  @process_cv.wait(@process_mutex)
@@ -2787,8 +3132,11 @@ module MarkdownExec
2787
3132
  @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution
2788
3133
  )
2789
3134
 
2790
- block_menu = prepare_blocks_menu(menu_blocks)
2791
- return SelectedBlockMenuState.new(nil, MenuState::EXIT) if block_menu.empty?
3135
+ menu_items = prepare_blocks_menu(menu_blocks)
3136
+ if menu_items.empty?
3137
+ return SelectedBlockMenuState.new(nil, OpenStruct.new,
3138
+ MenuState::EXIT)
3139
+ end
2792
3140
 
2793
3141
  # default value may not match if color is different from originating menu (opts changed while processing)
2794
3142
  selection_opts = if default && menu_blocks.map(&:dname).include?(default)
@@ -2800,7 +3148,7 @@ module MarkdownExec
2800
3148
  sph = @delegate_object[:select_page_height]
2801
3149
  selection_opts.merge!(per_page: sph)
2802
3150
 
2803
- selected_option = select_option_with_metadata(prompt_title, block_menu,
3151
+ selected_option = select_option_with_metadata(prompt_title, menu_items,
2804
3152
  selection_opts)
2805
3153
  determine_block_state(selected_option)
2806
3154
  end
@@ -2811,12 +3159,12 @@ module MarkdownExec
2811
3159
 
2812
3160
  time_now = Time.now.utc
2813
3161
  @run_state.saved_script_filename =
2814
- SavedAsset.script_name(
2815
- blockname: selected.pub_name,
2816
- filename: @delegate_object[:filename],
2817
- prefix: @delegate_object[:saved_script_filename_prefix],
2818
- time: time_now
2819
- )
3162
+ SavedAsset.new(blockname: selected.pub_name,
3163
+ exts: '.sh',
3164
+ filename: @delegate_object[:filename],
3165
+ prefix: @delegate_object[:saved_script_filename_prefix],
3166
+ saved_asset_format: shell_escape_asset_format(@dml_link_state),
3167
+ time: time_now).generate_name
2820
3168
  @run_state.saved_filespec =
2821
3169
  File.join(@delegate_object[:saved_script_folder],
2822
3170
  @run_state.saved_script_filename)
@@ -2866,7 +3214,8 @@ module MarkdownExec
2866
3214
  save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
2867
3215
  if save_expr.present?
2868
3216
  save_filespec = save_filespec_from_expression(save_expr)
2869
- File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
3217
+ File.write(save_filespec,
3218
+ HashDelegator.join_code_lines(link_state&.inherited_lines))
2870
3219
  @delegate_object[:filename]
2871
3220
  else
2872
3221
  link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
@@ -2898,7 +3247,11 @@ module MarkdownExec
2898
3247
  obj[key] = cleaned_value if value.is_a?(Hash) || value.is_a?(Struct)
2899
3248
  end
2900
3249
 
2901
- obj.reject! { |_key, value| [nil, '', [], {}, nil].include?(value) } if obj.is_a?(Hash)
3250
+ if obj.is_a?(Hash)
3251
+ obj.reject! do |_key, value|
3252
+ [nil, '', [], {}, nil].include?(value)
3253
+ end
3254
+ end
2902
3255
 
2903
3256
  obj
2904
3257
  end
@@ -2962,7 +3315,8 @@ module MarkdownExec
2962
3315
 
2963
3316
  # Test case for empty body
2964
3317
  def test_next_link_state
2965
- @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil, block_name: nil)
3318
+ @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil,
3319
+ block_name: nil)
2966
3320
  end
2967
3321
  end
2968
3322
 
@@ -3009,15 +3363,18 @@ module MarkdownExec
3009
3363
  # Test case for non-empty body with 'file' key
3010
3364
  def test_push_link_history_and_trigger_load_with_file_key
3011
3365
  body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
3012
- expected_result = LoadFileLinkState.new(LoadFile::LOAD,
3013
- LinkState.new(block_name: 'sample_block',
3014
- document_filename: 'sample_file',
3015
- inherited_dependencies: {},
3016
- inherited_lines: ['# ', 'KEY="VALUE"']))
3366
+ expected_result = LoadFileLinkState.new(
3367
+ LoadFile::LOAD,
3368
+ LinkState.new(block_name: 'sample_block',
3369
+ document_filename: 'sample_file',
3370
+ inherited_dependencies: {},
3371
+ inherited_lines: ['# ', 'KEY="VALUE"'])
3372
+ )
3017
3373
  assert_equal expected_result,
3018
3374
  @hd.push_link_history_and_trigger_load(
3019
3375
  link_block_body: body,
3020
- selected: FCB.new(block_name: 'sample_block', filename: 'sample_file')
3376
+ selected: FCB.new(block_name: 'sample_block',
3377
+ filename: 'sample_file')
3021
3378
  )
3022
3379
  end
3023
3380
 
@@ -3126,20 +3483,20 @@ module MarkdownExec
3126
3483
  end
3127
3484
 
3128
3485
  def test_block_find_with_match
3129
- blocks = [{ key: 'value1' }, { key: 'value2' }]
3130
- result = HashDelegator.block_find(blocks, :key, 'value1')
3131
- assert_equal({ key: 'value1' }, result)
3486
+ blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')]
3487
+ result = HashDelegator.block_find(blocks, :text, 'value1')
3488
+ assert_equal('value1', result.text)
3132
3489
  end
3133
3490
 
3134
3491
  def test_block_find_without_match
3135
- blocks = [{ key: 'value1' }, { key: 'value2' }]
3136
- result = HashDelegator.block_find(blocks, :key, 'value3')
3492
+ blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')]
3493
+ result = HashDelegator.block_find(blocks, :text, 'missing_value')
3137
3494
  assert_nil result
3138
3495
  end
3139
3496
 
3140
3497
  def test_block_find_with_default
3141
- blocks = [{ key: 'value1' }, { key: 'value2' }]
3142
- result = HashDelegator.block_find(blocks, :key, 'value3', 'default')
3498
+ blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')]
3499
+ result = HashDelegator.block_find(blocks, :text, 'missing_value', 'default')
3143
3500
  assert_equal 'default', result
3144
3501
  end
3145
3502
  end
@@ -3175,7 +3532,7 @@ module MarkdownExec
3175
3532
  @hd = HashDelegator.new
3176
3533
  @hd.instance_variable_set(:@delegate_object, {})
3177
3534
  @mdoc = mock('YourMDocClass')
3178
- @selected = { shell: BlockType::VARS, body: ['key: value'] }
3535
+ @selected = FCB.new(shell: BlockType::VARS, body: ['key: value'])
3179
3536
  HashDelegator.stubs(:read_required_blocks_from_temp_file).returns([])
3180
3537
  @hd.stubs(:string_send_color)
3181
3538
  @hd.stubs(:print)
@@ -3184,7 +3541,8 @@ module MarkdownExec
3184
3541
  def test_collect_required_code_lines_with_vars
3185
3542
  YAML.stubs(:load).returns({ 'key' => 'value' })
3186
3543
  @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] })
3187
- result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, block_source: {})
3544
+ result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected,
3545
+ block_source: {})
3188
3546
 
3189
3547
  assert_equal ['code line', 'key="value"'], result
3190
3548
  end
@@ -3205,18 +3563,24 @@ module MarkdownExec
3205
3563
 
3206
3564
  result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks)
3207
3565
 
3208
- assert_equal all_blocks.first.merge(block_name_from_ui: false), result.block
3566
+ assert_equal all_blocks.first,
3567
+ result.block
3568
+ assert_equal OpenStruct.new(block_name_from_ui: false),
3569
+ result.source
3209
3570
  assert_nil result.state
3210
3571
  end
3211
3572
 
3212
3573
  def test_user_selected_block
3213
- block_state = SelectedBlockMenuState.new({ oname: 'block2' },
3574
+ block_state = SelectedBlockMenuState.new({ oname: 'block2' }, OpenStruct.new,
3214
3575
  :some_state)
3215
3576
  @hd.stubs(:wait_for_user_selected_block).returns(block_state)
3216
3577
 
3217
3578
  result = @hd.load_cli_or_user_selected_block
3218
3579
 
3219
- assert_equal block_state.block.merge(block_name_from_ui: true), result.block
3580
+ assert_equal block_state.block,
3581
+ result.block
3582
+ assert_equal OpenStruct.new(block_name_from_ui: true),
3583
+ result.source
3220
3584
  assert_equal :some_state, result.state
3221
3585
  end
3222
3586
  end
@@ -3299,7 +3663,7 @@ module MarkdownExec
3299
3663
  end
3300
3664
 
3301
3665
  def test_determine_block_state_exit
3302
- selected_option = { oname: 'Formatted Option' }
3666
+ selected_option = FCB.new(oname: 'Formatted Option')
3303
3667
  @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_exit_name).returns('Formatted Option')
3304
3668
 
3305
3669
  result = @hd.determine_block_state(selected_option)
@@ -3309,7 +3673,7 @@ module MarkdownExec
3309
3673
  end
3310
3674
 
3311
3675
  def test_determine_block_state_back
3312
- selected_option = { oname: 'Formatted Back Option' }
3676
+ selected_option = FCB.new(oname: 'Formatted Back Option')
3313
3677
  @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('Formatted Back Option')
3314
3678
  result = @hd.determine_block_state(selected_option)
3315
3679
 
@@ -3318,7 +3682,7 @@ module MarkdownExec
3318
3682
  end
3319
3683
 
3320
3684
  def test_determine_block_state_continue
3321
- selected_option = { oname: 'Other Option' }
3685
+ selected_option = FCB.new(oname: 'Other Option')
3322
3686
 
3323
3687
  result = @hd.determine_block_state(selected_option)
3324
3688
 
@@ -3416,25 +3780,28 @@ module MarkdownExec
3416
3780
  @hd.instance_variable_set(:@run_state, mock('run_state'))
3417
3781
  end
3418
3782
 
3419
- def test_format_execution_streams_with_valid_key
3420
- result = HashDelegator.format_execution_streams(ExecutionStreams::STD_OUT,
3421
- { stdout: %w[output1 output2] })
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
+ )
3422
3788
 
3423
- assert_equal 'output1output2', result
3789
+ assert_equal "output1\noutput2", result
3424
3790
  end
3425
3791
 
3426
- def test_format_execution_streams_with_empty_key
3792
+ def test_format_execution_stream_with_empty_key
3427
3793
  @hd.instance_variable_get(:@run_state).stubs(:files).returns({})
3428
3794
 
3429
- result = HashDelegator.format_execution_streams(ExecutionStreams::STD_ERR)
3795
+ result = HashDelegator.format_execution_stream(nil,
3796
+ ExecutionStreams::STD_ERR)
3430
3797
 
3431
3798
  assert_equal '', result
3432
3799
  end
3433
3800
 
3434
- def test_format_execution_streams_with_nil_files
3801
+ def test_format_execution_stream_with_nil_files
3435
3802
  @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil)
3436
3803
 
3437
- result = HashDelegator.format_execution_streams(:stdin)
3804
+ result = HashDelegator.format_execution_stream(nil, :stdin)
3438
3805
 
3439
3806
  assert_equal '', result
3440
3807
  end
@@ -3585,7 +3952,8 @@ module MarkdownExec
3585
3952
  end
3586
3953
 
3587
3954
  def test_iter_blocks_from_nested_files
3588
- @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'], import_paths: nil)
3955
+ @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'],
3956
+ import_paths: nil)
3589
3957
  selected_messages = ['filtered message']
3590
3958
 
3591
3959
  result = @hd.iter_blocks_from_nested_files { selected_messages }
@@ -3691,7 +4059,8 @@ module MarkdownExec
3691
4059
 
3692
4060
  def test_yield_line_if_selected_with_line
3693
4061
  block_called = false
3694
- HashDelegator.yield_line_if_selected('Test line', [:line]) do |type, content|
4062
+ HashDelegator.yield_line_if_selected('Test line',
4063
+ [:line]) do |type, content|
3695
4064
  block_called = true
3696
4065
  assert_equal :line, type
3697
4066
  assert_equal 'Test line', content.body[0]
@@ -3726,15 +4095,18 @@ module MarkdownExec
3726
4095
  def test_update_menu_attrib_yield_selected_with_body
3727
4096
  HashDelegator.expects(:initialize_fcb_names).with(@fcb)
3728
4097
  HashDelegator.expects(:default_block_title_from_body).with(@fcb)
3729
- Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], {})
4098
+ Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message],
4099
+ {})
3730
4100
 
3731
- HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
4101
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb,
4102
+ messages: [:some_message])
3732
4103
  end
3733
4104
 
3734
4105
  def test_update_menu_attrib_yield_selected_without_body
3735
4106
  @fcb.stubs(:body).returns(nil)
3736
4107
  HashDelegator.expects(:initialize_fcb_names).with(@fcb)
3737
- HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
4108
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb,
4109
+ messages: [:some_message])
3738
4110
  end
3739
4111
  end
3740
4112
 
@@ -3810,28 +4182,35 @@ module MarkdownExec
3810
4182
  def test_absolute_path_returns_unchanged
3811
4183
  absolute_path = '/usr/local/bin'
3812
4184
  expression = 'path/to/*/directory'
3813
- assert_equal absolute_path, PathUtils.resolve_path_or_substitute(absolute_path, expression)
4185
+ assert_equal absolute_path,
4186
+ PathUtils.resolve_path_or_substitute(absolute_path,
4187
+ expression)
3814
4188
  end
3815
4189
 
3816
4190
  def test_relative_path_gets_substituted
3817
4191
  relative_path = 'my_folder'
3818
4192
  expression = 'path/to/*/directory'
3819
4193
  expected_output = 'path/to/my_folder/directory'
3820
- assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression)
4194
+ assert_equal expected_output,
4195
+ PathUtils.resolve_path_or_substitute(relative_path,
4196
+ expression)
3821
4197
  end
3822
4198
 
3823
4199
  def test_path_with_no_slash_substitutes_correctly
3824
4200
  relative_path = 'data'
3825
4201
  expression = 'path/to/*/directory'
3826
4202
  expected_output = 'path/to/data/directory'
3827
- assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression)
4203
+ assert_equal expected_output,
4204
+ PathUtils.resolve_path_or_substitute(relative_path,
4205
+ expression)
3828
4206
  end
3829
4207
 
3830
4208
  def test_empty_path_substitution
3831
4209
  empty_path = ''
3832
4210
  expression = 'path/to/*/directory'
3833
4211
  expected_output = 'path/to//directory'
3834
- assert_equal expected_output, PathUtils.resolve_path_or_substitute(empty_path, expression)
4212
+ assert_equal expected_output,
4213
+ PathUtils.resolve_path_or_substitute(empty_path, expression)
3835
4214
  end
3836
4215
 
3837
4216
  # Test formatting a string containing UTF-8 characters
@@ -3880,7 +4259,8 @@ module MarkdownExec
3880
4259
  private
3881
4260
 
3882
4261
  def prompt_for_filespec_with_wildcard(filespec)
3883
- puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
4262
+ puts format(@delegate_object[:prompt_show_expr_format],
4263
+ { expr: filespec })
3884
4264
  puts @delegate_object[:prompt_enter_filespec]
3885
4265
 
3886
4266
  begin