markdown_exec 2.0.8.4 → 2.2.0

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