markdown_exec 2.0.5 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -200,7 +200,7 @@ module HashDelegatorSelf
200
200
 
201
201
  # Determine the state of breaker based on was_using_cli and the block type
202
202
  # 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.
203
- breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH
203
+ breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
204
204
 
205
205
  # Reset block_name_from_cli if the conditions are not met
206
206
  block_name_from_cli ||= false
@@ -237,41 +237,17 @@ module HashDelegatorSelf
237
237
  # @param str [String] The string to be evaluated.
238
238
  # @return [Object] The result of evaluating the string.
239
239
  def safeval(str)
240
- # # Restricting to evaluate only expressions
241
- # unless str.match?(/\A\s*\w+\s*[\+\-\*\/\=\%\&\|\<\>\!]+\s*\w+\s*\z/)
242
- # error_handler('safeval') # 'Invalid expression'
243
- # return
244
- # end
245
-
246
- # # Whitelisting allowed operations
247
- # allowed_methods = %w[+ - * / == != < > <= >= && || % & |]
248
- # unless allowed_methods.any? { |op| str.include?(op) }
249
- # error_handler('safeval', 'Operation not allowed')
250
- # return
251
- # end
252
-
253
- # # Sanitize input (example: removing potentially harmful characters)
254
- # str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
255
-
256
- # Evaluate the sanitized string
257
240
  result = nil
258
241
  binding.eval("result = #{str}")
259
242
 
260
243
  result
261
244
  rescue StandardError # catches NameError, StandardError
245
+ pp $!, $@
246
+ pp "code: #{str}"
262
247
  error_handler('safeval')
248
+ exit 1
263
249
  end
264
250
 
265
- # # Evaluates the given string as Ruby code and rescues any StandardErrors.
266
- # # If an error occurs, it calls the error_handler method with 'safeval'.
267
- # # @param str [String] The string to be evaluated.
268
- # # @return [Object] The result of evaluating the string.
269
- # def safeval(str)
270
- # eval(str)
271
- # rescue StandardError # catches NameError, StandardError
272
- # error_handler('safeval')
273
- # end
274
-
275
251
  def set_file_permissions(file_path, chmod_value)
276
252
  File.chmod(chmod_value, file_path)
277
253
  end
@@ -311,11 +287,11 @@ module HashDelegatorSelf
311
287
  File.write(
312
288
  filespec,
313
289
  ["-STDOUT-\n",
314
- format_execution_streams(ExecutionStreams::StdOut, files),
290
+ format_execution_streams(ExecutionStreams::STD_OUT, files),
315
291
  "-STDERR-\n",
316
- format_execution_streams(ExecutionStreams::StdErr, files),
292
+ format_execution_streams(ExecutionStreams::STD_ERR, files),
317
293
  "-STDIN-\n",
318
- format_execution_streams(ExecutionStreams::StdIn, files),
294
+ format_execution_streams(ExecutionStreams::STD_IN, files),
319
295
  "\n"].join
320
296
  )
321
297
  end
@@ -387,7 +363,7 @@ module PathUtils
387
363
  # @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
388
364
  # @return [String] The absolute path or the expression with the wildcard replaced by the path.
389
365
  def self.resolve_path_or_substitute(path, expression)
390
- if path.include?('/')
366
+ if path.start_with?('/')
391
367
  path
392
368
  else
393
369
  expression.gsub('*', path)
@@ -509,9 +485,21 @@ module MarkdownExec
509
485
  history_state_partition
510
486
  option_name = @delegate_object[:menu_option_back_name]
511
487
  insert_at_top = @delegate_object[:menu_back_at_top]
488
+ when MenuState::EDIT
489
+ option_name = @delegate_object[:menu_option_edit_name]
490
+ insert_at_top = @delegate_object[:menu_load_at_top]
512
491
  when MenuState::EXIT
513
492
  option_name = @delegate_object[:menu_option_exit_name]
514
493
  insert_at_top = @delegate_object[:menu_exit_at_top]
494
+ when MenuState::LOAD
495
+ option_name = @delegate_object[:menu_option_load_name]
496
+ insert_at_top = @delegate_object[:menu_load_at_top]
497
+ when MenuState::SAVE
498
+ option_name = @delegate_object[:menu_option_save_name]
499
+ insert_at_top = @delegate_object[:menu_load_at_top]
500
+ when MenuState::VIEW
501
+ option_name = @delegate_object[:menu_option_view_name]
502
+ insert_at_top = @delegate_object[:menu_load_at_top]
515
503
  end
516
504
 
517
505
  formatted_name = format(@delegate_object[:menu_link_format],
@@ -519,7 +507,7 @@ module MarkdownExec
519
507
  chrome_block = FCB.new(
520
508
  chrome: true,
521
509
  dname: HashDelegator.new(@delegate_object).string_send_color(
522
- formatted_name, :menu_link_color
510
+ formatted_name, :menu_chrome_color
523
511
  ),
524
512
  oname: formatted_name
525
513
  )
@@ -617,6 +605,18 @@ module MarkdownExec
617
605
  HashDelegator.error_handler('blocks_from_nested_files')
618
606
  end
619
607
 
608
+ def block_state_for_name_from_cli(block_name)
609
+ SelectedBlockMenuState.new(
610
+ @dml_blocks_in_file.find do |item|
611
+ item[:oname] == block_name
612
+ end&.merge(
613
+ block_name_from_cli: true,
614
+ block_name_from_ui: false
615
+ ),
616
+ MenuState::CONTINUE
617
+ )
618
+ end
619
+
620
620
  # private
621
621
 
622
622
  def calc_logged_stdout_filename(block_name:)
@@ -725,14 +725,14 @@ module MarkdownExec
725
725
  '-c', command,
726
726
  @delegate_object[:filename],
727
727
  *args) do |stdin, stdout, stderr, exec_thr|
728
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
728
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
729
729
  yield nil, line, nil, exec_thr if block_given?
730
730
  end
731
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
731
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
732
732
  yield nil, nil, line, exec_thr if block_given?
733
733
  end
734
734
 
735
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
735
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
736
736
  stdin.puts(line)
737
737
  yield line, nil, nil, exec_thr if block_given?
738
738
  end
@@ -750,34 +750,17 @@ module MarkdownExec
750
750
  @run_state.aborted_at = Time.now.utc
751
751
  @run_state.error_message = err.message
752
752
  @run_state.error = err
753
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
753
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
754
754
  @fout.fout "Error ENOENT: #{err.inspect}"
755
755
  rescue SignalException => err
756
756
  # Handle SignalException
757
757
  @run_state.aborted_at = Time.now.utc
758
758
  @run_state.error_message = 'SIGTERM'
759
759
  @run_state.error = err
760
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
760
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
761
761
  @fout.fout "Error ENOENT: #{err.inspect}"
762
762
  end
763
763
 
764
- def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
765
- if @delegate_object[:block_name].present?
766
- block = all_blocks.find do |item|
767
- item[:oname] == @delegate_object[:block_name]
768
- end&.merge(block_name_from_ui: false)
769
- else
770
- block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
771
- default)
772
- block = block_state.block&.merge(block_name_from_ui: true)
773
- state = block_state.state
774
- end
775
-
776
- SelectedBlockMenuState.new(block, state)
777
- rescue StandardError
778
- HashDelegator.error_handler('load_cli_or_user_selected_block')
779
- end
780
-
781
764
  # This method is responsible for handling the execution of generic blocks in a markdown document.
782
765
  # It collects the required code lines from the document and, depending on the configuration,
783
766
  # may display the code for user approval before execution. It then executes the approved block.
@@ -799,7 +782,12 @@ module MarkdownExec
799
782
  execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
800
783
 
801
784
  link_state.block_name = nil
802
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
785
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
786
+ end
787
+
788
+ # Check if the expression contains wildcard characters
789
+ def contains_wildcards?(expr)
790
+ expr.match(%r{\*|\?|\[})
803
791
  end
804
792
 
805
793
  def copy_to_clipboard(required_lines)
@@ -950,6 +938,312 @@ module MarkdownExec
950
938
  @delegate_object[:logged_stdout_filespec])
951
939
  end
952
940
 
941
+ # Select and execute a code block from a Markdown document.
942
+ #
943
+ # This method allows the user to interactively select a code block from a
944
+ # Markdown document, obtain approval, and execute the chosen block of code.
945
+ #
946
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
947
+ def document_inpseq
948
+ @menu_base_options = @delegate_object
949
+ @dml_link_state = LinkState.new(
950
+ block_name: @delegate_object[:block_name],
951
+ document_filename: @delegate_object[:filename]
952
+ )
953
+ @run_state.block_name_from_cli = @dml_link_state.block_name.present?
954
+ @cli_block_name = @dml_link_state.block_name
955
+ @dml_now_using_cli = @run_state.block_name_from_cli
956
+ @dml_menu_default_dname = nil
957
+ @dml_block_state = SelectedBlockMenuState.new
958
+ @doc_saved_lines_files = []
959
+
960
+ ## load file with code lines per options
961
+ #
962
+ if @menu_base_options[:load_code].present?
963
+ @dml_link_state.inherited_lines = []
964
+ @menu_base_options[:load_code].split(':').map do |path|
965
+ @dml_link_state.inherited_lines += File.readlines(path, chomp: true)
966
+ end
967
+
968
+ inherited_block_names = []
969
+ inherited_dependencies = {}
970
+ selected = { oname: 'load_code' }
971
+ pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
972
+ end
973
+
974
+ item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
975
+ item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
976
+ item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
977
+ item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
978
+ item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
979
+
980
+ @run_state.batch_random = Random.new.rand
981
+ @run_state.batch_index = 0
982
+
983
+ InputSequencer.new(
984
+ @delegate_object[:filename],
985
+ @delegate_object[:input_cli_rest]
986
+ ).run do |msg, data|
987
+ case msg
988
+ when :parse_document # once for each menu
989
+ # puts "@ - parse document #{data}"
990
+ inpseq_parse_document(data)
991
+
992
+ if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
993
+
994
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
995
+ files = sf ? Dir.glob(sf) : []
996
+ @doc_saved_lines_files = files.count.positive? ? files : []
997
+
998
+ lines_count = @dml_link_state.inherited_lines&.count || 0
999
+
1000
+ # add menu items (glob, load, save) and enable selectively
1001
+ menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
1002
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD)
1003
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT)
1004
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), lines_count, 'lines', menu_state: MenuState::SAVE)
1005
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), lines_count, 'lines', menu_state: MenuState::VIEW)
1006
+ end
1007
+
1008
+ when :display_menu
1009
+ # warn "@ - display menu:"
1010
+ # ii_display_menu
1011
+ @dml_block_state = SelectedBlockMenuState.new
1012
+ @delegate_object[:block_name] = nil
1013
+
1014
+ when :user_choice
1015
+ if @dml_link_state.block_name.present?
1016
+ # @prior_block_was_link = true
1017
+ @dml_block_state.block = @dml_blocks_in_file.find do |item|
1018
+ item[:oname] == @dml_link_state.block_name
1019
+ end
1020
+ @dml_link_state.block_name = nil
1021
+ else
1022
+ # puts "? - Select a block to execute (or type #{$texit} to exit):"
1023
+ break if inpseq_user_choice == :break # into @dml_block_state
1024
+ break if @dml_block_state.block.nil? # no block matched
1025
+ end
1026
+ # puts "! - Executing block: #{data}"
1027
+ # @dml_block_state.block[:oname]
1028
+ @dml_block_state.block&.fetch(:oname, nil)
1029
+
1030
+ when :execute_block
1031
+ case (block_name = data)
1032
+ when item_back
1033
+ debounce_reset
1034
+ @menu_user_clicked_back_link = true
1035
+ load_file_link_state = pop_link_history_and_trigger_load
1036
+ @dml_link_state = load_file_link_state.link_state
1037
+
1038
+ InputSequencer.merge_link_state(
1039
+ @dml_link_state,
1040
+ InputSequencer.next_link_state(
1041
+ block_name: @dml_link_state.block_name,
1042
+ document_filename: @dml_link_state.document_filename,
1043
+ prior_block_was_link: true
1044
+ )
1045
+ )
1046
+
1047
+ when item_edit
1048
+ edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
1049
+ @dml_link_state.inherited_lines = edited.split("\n") if edited
1050
+ InputSequencer.next_link_state(prior_block_was_link: true)
1051
+
1052
+ when item_load
1053
+ debounce_reset
1054
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1055
+ load_filespec = load_filespec_from_expression(sf)
1056
+ if load_filespec
1057
+ @dml_link_state.inherited_lines ||= []
1058
+ @dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
1059
+ end
1060
+ InputSequencer.next_link_state(prior_block_was_link: true)
1061
+
1062
+ when item_save
1063
+ debounce_reset
1064
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1065
+ save_filespec = save_filespec_from_expression(sf)
1066
+ if save_filespec && !write_file_with_directory_creation(
1067
+ save_filespec,
1068
+ HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
1069
+ )
1070
+ return :break
1071
+
1072
+ end
1073
+
1074
+ InputSequencer.next_link_state(prior_block_was_link: true)
1075
+
1076
+ when item_view
1077
+ warn @dml_link_state.inherited_lines.join("\n")
1078
+ InputSequencer.next_link_state(prior_block_was_link: true)
1079
+
1080
+ else
1081
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1082
+ if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
1083
+ debounce_reset
1084
+ link_state = LinkState.new
1085
+ options_state = read_show_options_and_trigger_reuse(
1086
+ selected: @dml_block_state.block,
1087
+ link_state: link_state
1088
+ )
1089
+
1090
+ @menu_base_options.merge!(options_state.options)
1091
+ @delegate_object.merge!(options_state.options)
1092
+ options_state.load_file_link_state.link_state
1093
+ else
1094
+ inpseq_execute_block(block_name)
1095
+
1096
+ if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1097
+ selected: @dml_block_state.block)
1098
+ return :break
1099
+ end
1100
+
1101
+ ## order of block name processing: link block, cli, from user
1102
+ #
1103
+ @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1104
+ HashDelegator.next_link_state(
1105
+ block_name: @dml_link_state.block_name,
1106
+ block_name_from_cli: !@dml_link_state.block_name.present?,
1107
+ block_state: @dml_block_state,
1108
+ was_using_cli: @dml_now_using_cli
1109
+ )
1110
+
1111
+ if !@dml_block_state.block[:block_name_from_ui] && cli_break
1112
+ # &bsp '!block_name_from_ui + cli_break -> break'
1113
+ return :break
1114
+ end
1115
+
1116
+ InputSequencer.next_link_state(
1117
+ block_name: @dml_link_state.block_name,
1118
+ prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
1119
+ )
1120
+ end
1121
+ end
1122
+
1123
+ when :exit?
1124
+ data == $texit
1125
+ when :stay?
1126
+ data == $stay
1127
+ else
1128
+ raise "Invalid message: #{msg}"
1129
+ end
1130
+ end
1131
+ rescue StandardError
1132
+ HashDelegator.error_handler('document_inpseq',
1133
+ { abort: true })
1134
+ end
1135
+
1136
+ # remove leading "./"
1137
+ # replace characters: / : . * (space) with: (underscore)
1138
+ def document_name_in_glob_as_file_name(document_filename, glob)
1139
+ return document_filename if document_filename.nil? || document_filename.empty?
1140
+
1141
+ format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
1142
+ end
1143
+
1144
+ def dump_and_warn_block_state(selected:)
1145
+ if selected.nil?
1146
+ Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
1147
+ { abort: true })
1148
+ end
1149
+
1150
+ return unless @delegate_object[:dump_selected_block]
1151
+
1152
+ warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
1153
+ end
1154
+
1155
+ # Outputs warnings based on the delegate object's configuration
1156
+ #
1157
+ # @param delegate_object [Hash] The delegate object containing configuration flags.
1158
+ # @param blocks_in_file [Hash] Hash of blocks present in the file.
1159
+ # @param menu_blocks [Hash] Hash of menu blocks.
1160
+ # @param link_state [LinkState] Current state of the link.
1161
+ def dump_delobj(blocks_in_file, menu_blocks, link_state)
1162
+ warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
1163
+
1164
+ if @delegate_object[:dump_blocks_in_file]
1165
+ warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
1166
+ label: 'blocks_in_file')
1167
+ end
1168
+
1169
+ if @delegate_object[:dump_menu_blocks]
1170
+ warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
1171
+ label: 'menu_blocks')
1172
+ end
1173
+
1174
+ warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
1175
+ warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
1176
+ return unless @delegate_object[:dump_inherited_lines]
1177
+
1178
+ warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
1179
+ end
1180
+
1181
+ # Opens text in an editor for user modification and returns the modified text.
1182
+ #
1183
+ # This method reads the provided text, opens it in the default editor,
1184
+ # and allows the user to modify it. If the user makes changes, the
1185
+ # modified text is returned. If the user exits the editor without
1186
+ # making changes or the editor is closed abruptly, appropriate messages
1187
+ # are displayed.
1188
+ #
1189
+ # @param [String] initial_text The initial text to be edited.
1190
+ # @param [String] temp_name The base name for the temporary file (default: 'edit_text').
1191
+ # @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly.
1192
+ def edit_text(initial_text, temp_name: 'edit_text')
1193
+ # Create a temporary file to store the initial text
1194
+ temp_file = Tempfile.new(temp_name)
1195
+ temp_file.write(initial_text)
1196
+ temp_file.rewind
1197
+
1198
+ # Capture the modification time of the temporary file before editing
1199
+ before_mtime = temp_file.mtime
1200
+
1201
+ # Open the temporary file in the default editor
1202
+ system("#{ENV['EDITOR'] || 'vi'} #{temp_file.path}")
1203
+
1204
+ # Capture the exit status of the editor
1205
+ editor_exit_status = $?.exitstatus
1206
+
1207
+ # Reopen the file to ensure the updated modification time is read
1208
+ temp_file.open
1209
+ after_mtime = temp_file.mtime
1210
+
1211
+ # Check if the editor was exited normally or was interrupted
1212
+ if editor_exit_status != 0
1213
+ warn 'The editor was closed abruptly. No changes were made.'
1214
+ temp_file.close
1215
+ temp_file.unlink
1216
+ return
1217
+ end
1218
+
1219
+ result_text = nil
1220
+ # Read the file if it was modified
1221
+ if before_mtime != after_mtime
1222
+ temp_file.rewind
1223
+ result_text = temp_file.read
1224
+ end
1225
+
1226
+ # Remove the temporary file
1227
+ temp_file.close
1228
+ temp_file.unlink
1229
+
1230
+ result_text
1231
+ end
1232
+
1233
+ def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
1234
+ lfls = execute_shell_type(
1235
+ selected: selected,
1236
+ mdoc: mdoc,
1237
+ link_state: link_state,
1238
+ block_source: block_source
1239
+ )
1240
+
1241
+ # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1242
+ [lfls.link_state,
1243
+ lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
1244
+ #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1245
+ end
1246
+
953
1247
  # Executes a block of code that has been approved for execution.
954
1248
  # It sets the script block name, writes command files if required, and handles the execution
955
1249
  # including output formatting and summarization.
@@ -1007,7 +1301,7 @@ module MarkdownExec
1007
1301
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1008
1302
  next_block_name: '',
1009
1303
  next_document_filename: @delegate_object[:filename],
1010
- next_load_file: LoadFile::Reuse
1304
+ next_load_file: LoadFile::REUSE
1011
1305
  )
1012
1306
 
1013
1307
  elsif selected[:shell] == BlockType::VARS
@@ -1023,7 +1317,7 @@ module MarkdownExec
1023
1317
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1024
1318
  next_block_name: '',
1025
1319
  next_document_filename: @delegate_object[:filename],
1026
- next_load_file: LoadFile::Reuse
1320
+ next_load_file: LoadFile::REUSE
1027
1321
  )
1028
1322
 
1029
1323
  elsif debounce_allows
@@ -1033,7 +1327,7 @@ module MarkdownExec
1033
1327
  block_source: block_source)
1034
1328
 
1035
1329
  else
1036
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
1330
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
1037
1331
  end
1038
1332
  end
1039
1333
 
@@ -1060,6 +1354,23 @@ module MarkdownExec
1060
1354
  color_sym: :script_execution_frame_color)
1061
1355
  end
1062
1356
 
1357
+ # Format expression using environment variables and run state
1358
+ def format_expression(expr)
1359
+ data = link_load_format_data
1360
+ ENV.each { |key, value| data[key] = value }
1361
+ format(expr, data)
1362
+ end
1363
+
1364
+ # Formats multiline body content as a title string.
1365
+ # indents all but first line with two spaces so it displays correctly in menu
1366
+ # @param body_lines [Array<String>] The lines of body content.
1367
+ # @return [String] Formatted title.
1368
+ def format_multiline_body_as_title(body_lines)
1369
+ body_lines.map.with_index do |line, index|
1370
+ index.zero? ? line : " #{line}"
1371
+ end.join("\n") + "\n"
1372
+ end
1373
+
1063
1374
  # Formats a string based on a given context and applies color styling to it.
1064
1375
  # It retrieves format and color information from the delegate object and processes accordingly.
1065
1376
  #
@@ -1076,9 +1387,14 @@ module MarkdownExec
1076
1387
  string_send_color(formatted_string, color_sym)
1077
1388
  end
1078
1389
 
1079
- # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1080
- # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1081
- #
1390
+ # Expand expression if it contains format specifiers
1391
+ def formatted_expression(expr)
1392
+ expr.include?('%{') ? format_expression(expr) : expr
1393
+ end
1394
+
1395
+ # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1396
+ # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1397
+ #
1082
1398
  # @param fcb [Object] An object representing a functional code block.
1083
1399
  # @return [Object] The modified functional code block with updated summary attributes.
1084
1400
  def get_block_summary(fcb)
@@ -1110,16 +1426,6 @@ module MarkdownExec
1110
1426
  fcb
1111
1427
  end
1112
1428
 
1113
- # Formats multiline body content as a title string.
1114
- # indents all but first line with two spaces so it displays correctly in menu
1115
- # @param body_lines [Array<String>] The lines of body content.
1116
- # @return [String] Formatted title.
1117
- def format_multiline_body_as_title(body_lines)
1118
- body_lines.map.with_index do |line, index|
1119
- index.zero? ? line : " #{line}"
1120
- end.join("\n") + "\n"
1121
- end
1122
-
1123
1429
  # Updates the delegate object's state based on the provided block state.
1124
1430
  # It sets the block name and determines if the user clicked the back link in the menu.
1125
1431
  #
@@ -1172,6 +1478,46 @@ module MarkdownExec
1172
1478
  }
1173
1479
  end
1174
1480
 
1481
+ def inpseq_execute_block(block_name)
1482
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1483
+
1484
+ dump_and_warn_block_state(selected: @dml_block_state.block)
1485
+ @dml_link_state, @dml_menu_default_dname = \
1486
+ exec_bash_next_state(
1487
+ selected: @dml_block_state.block,
1488
+ mdoc: @dml_mdoc,
1489
+ link_state: @dml_link_state,
1490
+ block_source: {
1491
+ document_filename: @delegate_object[:filename],
1492
+ time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1493
+ }
1494
+ )
1495
+ end
1496
+
1497
+ def inpseq_parse_document(_document_filename)
1498
+ @run_state.batch_index += 1
1499
+ @run_state.in_own_window = false
1500
+
1501
+ # &bsp 'loop', block_name_from_cli, @cli_block_name
1502
+ @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
1503
+ set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
1504
+ now_using_cli: @dml_now_using_cli,
1505
+ link_state: @dml_link_state)
1506
+ end
1507
+
1508
+ def inpseq_user_choice
1509
+ @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1510
+ menu_blocks: @dml_menu_blocks,
1511
+ default: @dml_menu_default_dname)
1512
+ # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1513
+ if !@dml_block_state
1514
+ HashDelegator.error_handler('block_state missing', { abort: true })
1515
+ elsif @dml_block_state.state == MenuState::EXIT
1516
+ # &bsp 'load_cli_or_user_selected_block -> break'
1517
+ :break
1518
+ end
1519
+ end
1520
+
1175
1521
  # Iterates through blocks in a file, applying the provided block to each line.
1176
1522
  # The iteration only occurs if the file exists.
1177
1523
  # @yield [Symbol] :filter Yields to obtain selected messages for processing.
@@ -1198,18 +1544,18 @@ module MarkdownExec
1198
1544
  file.write(all_code.join("\n"))
1199
1545
  file.rewind
1200
1546
 
1201
- if link_block_data.fetch(LinkKeys::Exec, false)
1547
+ if link_block_data.fetch(LinkKeys::EXEC, false)
1202
1548
  @run_state.files = Hash.new([])
1203
1549
 
1204
1550
  Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
1205
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
1551
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
1206
1552
  output_lines.push(line)
1207
1553
  end
1208
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
1554
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
1209
1555
  output_lines.push(line)
1210
1556
  end
1211
1557
 
1212
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
1558
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
1213
1559
  stdin.puts(line)
1214
1560
  end
1215
1561
 
@@ -1275,114 +1621,6 @@ module MarkdownExec
1275
1621
  )
1276
1622
  end
1277
1623
 
1278
- # format + glob + select for file in load block
1279
- # name has references to ENV vars and doc and batch vars incl. timestamp
1280
- def load_filespec_from_expression(expression)
1281
- # Process expression with embedded formatting
1282
- expanded_expression = formatted_expression(expression)
1283
-
1284
- # Handle wildcards or direct file specification
1285
- if contains_wildcards?(expanded_expression)
1286
- load_filespec_wildcard_expansion(expanded_expression)
1287
- else
1288
- expanded_expression
1289
- end
1290
- end
1291
-
1292
- def save_filespec_from_expression(expression)
1293
- # Process expression with embedded formatting
1294
- formatted = formatted_expression(expression)
1295
-
1296
- # Handle wildcards or direct file specification
1297
- if contains_wildcards?(formatted)
1298
- save_filespec_wildcard_expansion(formatted)
1299
- else
1300
- formatted
1301
- end
1302
- end
1303
-
1304
- # private
1305
-
1306
- # Expand expression if it contains format specifiers
1307
- def formatted_expression(expr)
1308
- expr.include?('%{') ? format_expression(expr) : expr
1309
- end
1310
-
1311
- # Format expression using environment variables and run state
1312
- def format_expression(expr)
1313
- data = link_load_format_data
1314
- ENV.each { |key, value| data[key] = value }
1315
- format(expr, data)
1316
- end
1317
-
1318
- # Check if the expression contains wildcard characters
1319
- def contains_wildcards?(expr)
1320
- expr.match(%r{\*|\?|\[})
1321
- end
1322
-
1323
- # Handle expression with wildcard characters
1324
- def load_filespec_wildcard_expansion(expr)
1325
- files = find_files(expr)
1326
- case files.count
1327
- when 0
1328
- HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
1329
- when 1
1330
- files.first
1331
- else
1332
- prompt_select_code_filename(files)
1333
- end
1334
- end
1335
-
1336
- # Handle expression with wildcard characters
1337
- # allow user to select or enter
1338
- def puts_gets_oprompt_(filespec)
1339
- puts format(@delegate_object[:prompt_show_expr_format],
1340
- { expr: filespec })
1341
- puts @delegate_object[:prompt_enter_filespec]
1342
- gets.chomp
1343
- end
1344
-
1345
- # prompt user to enter a path (i.e. containing a path separator)
1346
- # or name to substitute into the wildcard expression
1347
- def prompt_for_filespec_with_wildcard(filespec)
1348
- puts format(@delegate_object[:prompt_show_expr_format],
1349
- { expr: filespec })
1350
- puts @delegate_object[:prompt_enter_filespec]
1351
- PathUtils.resolve_path_or_substitute(gets.chomp, filespec)
1352
- end
1353
-
1354
- # def read_block_name(line)
1355
- # bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
1356
- # name = bm[:title]
1357
-
1358
- # if @delegate_object[:block_name_nick_match].present? && line =~ Regexp.new(@delegate_object[:block_name_nick_match])
1359
- # name = $~[0]
1360
- # else
1361
- # name = bm && bm[1] ? bm[:title] : name
1362
- # end
1363
- # name
1364
- # end
1365
-
1366
- # Handle expression with wildcard characters
1367
- # allow user to select or enter
1368
- def save_filespec_wildcard_expansion(filespec)
1369
- files = find_files(filespec)
1370
- case files.count
1371
- when 0
1372
- prompt_for_filespec_with_wildcard(filespec)
1373
- else
1374
- ## user selects from existing files or other
1375
- # input into path with wildcard for easy entry
1376
- #
1377
- name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
1378
- if name == @delegate_object[:prompt_filespec_other]
1379
- prompt_for_filespec_with_wildcard(filespec)
1380
- else
1381
- name
1382
- end
1383
- end
1384
- end
1385
-
1386
1624
  def link_load_format_data
1387
1625
  {
1388
1626
  batch_index: @run_state.batch_index,
@@ -1395,29 +1633,6 @@ module MarkdownExec
1395
1633
  }
1396
1634
  end
1397
1635
 
1398
- # # Loads auto link block.
1399
- # def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
1400
- # block_name = @delegate_object[:document_load_link_block_name]
1401
- # return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1402
-
1403
- # block = HashDelegator.block_find(all_blocks, :oname, block_name)
1404
- # return unless block
1405
-
1406
- # if block.fetch(:shell, '') != BlockType::LINK
1407
- # HashDelegator.error_handler('must be Link block type', { abort: true })
1408
-
1409
- # else
1410
- # # debounce_reset
1411
- # push_link_history_and_trigger_load(
1412
- # link_block_body: block.fetch(:body, ''),
1413
- # mdoc: mdoc,
1414
- # selected: block,
1415
- # link_state: link_state,
1416
- # block_source: block_source
1417
- # )
1418
- # end
1419
- # end
1420
-
1421
1636
  # Loads auto blocks based on delegate object settings and updates if new filename is detected.
1422
1637
  # Executes a specified block once per filename.
1423
1638
  # @param all_blocks [Array] Array of all block elements.
@@ -1437,6 +1652,55 @@ module MarkdownExec
1437
1652
  true
1438
1653
  end
1439
1654
 
1655
+ def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
1656
+ if @delegate_object[:block_name].present?
1657
+ block = all_blocks.find do |item|
1658
+ item[:oname] == @delegate_object[:block_name]
1659
+ end&.merge(block_name_from_ui: false)
1660
+ else
1661
+ block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
1662
+ default)
1663
+ block = block_state.block&.merge(block_name_from_ui: true)
1664
+ state = block_state.state
1665
+ end
1666
+
1667
+ SelectedBlockMenuState.new(block, state)
1668
+ rescue StandardError
1669
+ HashDelegator.error_handler('load_cli_or_user_selected_block')
1670
+ end
1671
+
1672
+ # format + glob + select for file in load block
1673
+ # name has references to ENV vars and doc and batch vars incl. timestamp
1674
+ def load_filespec_from_expression(expression)
1675
+ # Process expression with embedded formatting
1676
+ expanded_expression = formatted_expression(expression)
1677
+
1678
+ # Handle wildcards or direct file specification
1679
+ if contains_wildcards?(expanded_expression)
1680
+ load_filespec_wildcard_expansion(expanded_expression)
1681
+ else
1682
+ expanded_expression
1683
+ end
1684
+ end
1685
+ # Handle expression with wildcard characters
1686
+ def load_filespec_wildcard_expansion(expr, auto_load_single: false)
1687
+ files = find_files(expr)
1688
+ if files.count.zero?
1689
+ HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
1690
+ elsif auto_load_single && files.count == 1
1691
+ files.first
1692
+ else
1693
+ ## user selects from existing files or other
1694
+ #
1695
+ case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
1696
+ when @delegate_object[:prompt_filespec_back]
1697
+ # do nothing
1698
+ else
1699
+ name
1700
+ end
1701
+ end
1702
+ end
1703
+
1440
1704
  def mdoc_and_blocks_from_nested_files
1441
1705
  menu_blocks = blocks_from_nested_files
1442
1706
  mdoc = MDoc.new(menu_blocks) do |nopts|
@@ -1453,15 +1717,41 @@ module MarkdownExec
1453
1717
  # recreate menu with new options
1454
1718
  #
1455
1719
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
1456
- # load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
1457
1720
 
1458
1721
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1459
1722
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
1460
1723
  ### compress empty lines
1461
- HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true
1724
+ HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
1462
1725
  [all_blocks, menu_blocks, mdoc]
1463
1726
  end
1464
1727
 
1728
+ def menu_add_disabled_option(name)
1729
+ raise unless name.present?
1730
+ raise if @dml_menu_blocks.nil?
1731
+
1732
+ block = @dml_menu_blocks.find { |item| item[:oname] == name }
1733
+
1734
+ # create menu item when it is needed (count > 0)
1735
+ #
1736
+ return unless block.nil?
1737
+
1738
+ # append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD)
1739
+ chrome_block = FCB.new(
1740
+ chrome: true,
1741
+ disabled: '',
1742
+ dname: HashDelegator.new(@delegate_object).string_send_color(
1743
+ name, :menu_inherited_lines_color
1744
+ ),
1745
+ oname: formatted_name
1746
+ )
1747
+
1748
+ if insert_at_top
1749
+ @dml_menu_blocks.unshift(chrome_block)
1750
+ else
1751
+ @dml_menu_blocks.push(chrome_block)
1752
+ end
1753
+ end
1754
+
1465
1755
  # Formats and optionally colors a menu option based on delegate object's configuration.
1466
1756
  # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
1467
1757
  # @return [String] The formatted and possibly colored value of the menu option.
@@ -1486,6 +1776,47 @@ module MarkdownExec
1486
1776
  end
1487
1777
  end
1488
1778
 
1779
+ def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
1780
+ raise unless name.present?
1781
+ raise if @dml_menu_blocks.nil?
1782
+
1783
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
1784
+
1785
+ # create menu item when it is needed (count > 0)
1786
+ #
1787
+ if item.nil? && count.positive?
1788
+ append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
1789
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
1790
+ end
1791
+
1792
+ # update item if it exists
1793
+ #
1794
+ return unless item
1795
+
1796
+ item[:dname] = "#{name} (#{count} #{type})"
1797
+ if count.positive?
1798
+ item.delete(:disabled)
1799
+ else
1800
+ item[:disabled] = ''
1801
+ end
1802
+ end
1803
+
1804
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
1805
+ if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
1806
+ # &bsp 'pause cli control, allow user to select block'
1807
+ block_name_from_cli = false
1808
+ now_using_cli = false
1809
+ @menu_base_options[:block_name] = \
1810
+ @delegate_object[:block_name] = \
1811
+ link_state.block_name = \
1812
+ @cli_block_name = nil
1813
+ end
1814
+
1815
+ @delegate_object = @menu_base_options.dup
1816
+ @menu_user_clicked_back_link = false
1817
+ [block_name_from_cli, now_using_cli]
1818
+ end
1819
+
1489
1820
  # If a method is missing, treat it as a key for the @delegate_object.
1490
1821
  def method_missing(method_name, *args, &block)
1491
1822
  if @delegate_object.respond_to?(method_name)
@@ -1567,7 +1898,7 @@ module MarkdownExec
1567
1898
  @link_history.push(next_state)
1568
1899
 
1569
1900
  next_state.block_name = nil
1570
- LoadFileLinkState.new(LoadFile::Load, next_state)
1901
+ LoadFileLinkState.new(LoadFile::LOAD, next_state)
1571
1902
  else
1572
1903
  # no history exists; must have been called independently => retain script
1573
1904
  link_history_push_and_next(
@@ -1576,11 +1907,11 @@ module MarkdownExec
1576
1907
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1577
1908
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1578
1909
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1579
- next_block_name: '', # not link_block_data[LinkKeys::Block] || ''
1910
+ next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || ''
1580
1911
  next_document_filename: @delegate_object[:filename], # not next_document_filename
1581
- next_load_file: LoadFile::Reuse # not next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1912
+ next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
1582
1913
  )
1583
- # LoadFileLinkState.new(LoadFile::Reuse, link_state)
1914
+ # LoadFileLinkState.new(LoadFile::REUSE, link_state)
1584
1915
  end
1585
1916
  end
1586
1917
 
@@ -1591,7 +1922,7 @@ module MarkdownExec
1591
1922
  def pop_link_history_and_trigger_load
1592
1923
  pop = @link_history.pop
1593
1924
  peek = @link_history.peek
1594
- LoadFileLinkState.new(LoadFile::Load, LinkState.new(
1925
+ LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
1595
1926
  document_filename: pop.document_filename,
1596
1927
  inherited_block_names: peek.inherited_block_names,
1597
1928
  inherited_dependencies: peek.inherited_dependencies,
@@ -1705,6 +2036,24 @@ module MarkdownExec
1705
2036
  exit 1
1706
2037
  end
1707
2038
 
2039
+ # Prompts the user to enter a path or name to substitute into the wildcard expression.
2040
+ # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
2041
+ #
2042
+ # @param filespec [String] the wildcard expression to be substituted
2043
+ # @return [String, nil] the resolved path or substituted expression, or nil if interrupted
2044
+ def prompt_for_filespec_with_wildcard(filespec)
2045
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
2046
+ puts @delegate_object[:prompt_enter_filespec]
2047
+
2048
+ begin
2049
+ input = gets.chomp
2050
+ PathUtils.resolve_path_or_substitute(input, filespec)
2051
+ rescue Interrupt
2052
+ puts "\nOperation interrupted. Returning nil."
2053
+ nil
2054
+ end
2055
+ end
2056
+
1708
2057
  ##
1709
2058
  # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1710
2059
  # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
@@ -1748,38 +2097,47 @@ module MarkdownExec
1748
2097
  exit 1
1749
2098
  end
1750
2099
 
1751
- def prompt_select_continue
1752
- sel = @prompt.select(
1753
- string_send_color(@delegate_object[:prompt_after_script_execution],
2100
+ # public
2101
+
2102
+ def prompt_select_code_filename(filenames)
2103
+ @prompt.select(
2104
+ string_send_color(@delegate_object[:prompt_select_code_file],
1754
2105
  :prompt_color_after_script_execution),
1755
2106
  filter: true,
1756
2107
  quiet: true
1757
2108
  ) do |menu|
1758
- menu.choice @delegate_object[:prompt_yes]
1759
- menu.choice @delegate_object[:prompt_exit]
1760
- end
1761
- sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
2109
+ filenames.each do |filename|
2110
+ menu.choice filename
2111
+ end
2112
+ end
1762
2113
  rescue TTY::Reader::InputInterrupt
1763
2114
  exit 1
1764
2115
  end
1765
2116
 
1766
- # public
1767
-
1768
- def prompt_select_code_filename(filenames)
1769
- @prompt.select(
1770
- string_send_color(@delegate_object[:prompt_select_code_file],
2117
+ def prompt_select_continue
2118
+ sel = @prompt.select(
2119
+ string_send_color(@delegate_object[:prompt_after_script_execution],
1771
2120
  :prompt_color_after_script_execution),
1772
2121
  filter: true,
1773
2122
  quiet: true
1774
2123
  ) do |menu|
1775
- filenames.each do |filename|
1776
- menu.choice filename
1777
- end
2124
+ menu.choice @delegate_object[:prompt_yes]
2125
+ menu.choice @delegate_object[:prompt_exit]
1778
2126
  end
2127
+ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1779
2128
  rescue TTY::Reader::InputInterrupt
1780
2129
  exit 1
1781
2130
  end
1782
2131
 
2132
+ # user prompt to exit if the menu will be displayed again
2133
+ #
2134
+ def prompt_user_exit(block_name_from_cli:, selected:)
2135
+ !block_name_from_cli &&
2136
+ selected[:shell] == BlockType::BASH &&
2137
+ @delegate_object[:pause_after_script_execution] &&
2138
+ prompt_select_continue == MenuState::EXIT
2139
+ end
2140
+
1783
2141
  # Handles the processing of a link block in Markdown Execution.
1784
2142
  # It loads YAML data from the link_block_body content, pushes the state to history,
1785
2143
  # sets environment variables, and decides on the next block to load.
@@ -1812,368 +2170,153 @@ module MarkdownExec
1812
2170
 
1813
2171
  # load key and values from link block into current environment
1814
2172
  #
1815
- if link_block_data[LinkKeys::Vars]
2173
+ if link_block_data[LinkKeys::VARS]
1816
2174
  code_lines.push BashCommentFormatter.format_comment(selected[:oname])
1817
- (link_block_data[LinkKeys::Vars] || []).each do |(key, value)|
1818
- ENV[key] = value.to_s
1819
- code_lines.push(assign_key_value_in_bash(key, value))
1820
- end
1821
- end
1822
-
1823
- ## append blocks loaded, apply LinkKeys::Eval
1824
- #
1825
- if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
1826
- load_filespec = load_filespec_from_expression(load_expr)
1827
- code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
1828
- end
1829
-
1830
- # if an eval link block, evaluate code_lines and return its standard output
1831
- #
1832
- if link_block_data.fetch(LinkKeys::Eval,
1833
- false) || link_block_data.fetch(LinkKeys::Exec, false)
1834
- code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
1835
- end
1836
-
1837
- next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
1838
-
1839
- if link_block_data[LinkKeys::Return]
1840
- pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
1841
- dependencies, selected)
1842
-
1843
- else
1844
- link_history_push_and_next(
1845
- curr_block_name: selected[:oname],
1846
- curr_document_filename: @delegate_object[:filename],
1847
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1848
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1849
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1850
- next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
1851
- nil) || link_block_data[LinkKeys::Block] || '',
1852
- next_document_filename: next_document_filename,
1853
- next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1854
- )
1855
- end
1856
- end
1857
-
1858
- # Check if the delegate object responds to a given method.
1859
- # @param method_name [Symbol] The name of the method to check.
1860
- # @param include_private [Boolean] Whether to include private methods in the check.
1861
- # @return [Boolean] true if the delegate object responds to the method, false otherwise.
1862
- def respond_to?(method_name, include_private = false)
1863
- if super
1864
- true
1865
- elsif @delegate_object.respond_to?(method_name, include_private)
1866
- true
1867
- elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
1868
- true
1869
- else
1870
- @delegate_object.respond_to?(method_name, include_private)
1871
- end
1872
- end
1873
-
1874
- def runtime_exception(exception_sym, name, items)
1875
- if @delegate_object[exception_sym] != 0
1876
- data = { name: name, detail: items.join(', ') }
1877
- warn(
1878
- format(
1879
- @delegate_object.fetch(:exception_format_name, "\n%{name}"),
1880
- data
1881
- ).send(@delegate_object.fetch(:exception_color_name, :red)) +
1882
- format(
1883
- @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
1884
- data
1885
- ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
1886
- )
1887
- end
1888
- return unless (@delegate_object[exception_sym]).positive?
1889
-
1890
- exit @delegate_object[exception_sym]
1891
- end
1892
-
1893
- def save_to_file(required_lines:, selected:)
1894
- write_command_file(required_lines: required_lines, selected: selected)
1895
- @fout.fout "File saved: #{@run_state.saved_filespec}"
1896
- end
1897
-
1898
- def block_state_for_name_from_cli(block_name)
1899
- SelectedBlockMenuState.new(
1900
- @dml_blocks_in_file.find do |item|
1901
- item[:oname] == block_name
1902
- end&.merge(
1903
- block_name_from_cli: true,
1904
- block_name_from_ui: false
1905
- ),
1906
- MenuState::CONTINUE
1907
- )
1908
- end
1909
-
1910
- # Select and execute a code block from a Markdown document.
1911
- #
1912
- # This method allows the user to interactively select a code block from a
1913
- # Markdown document, obtain approval, and execute the chosen block of code.
1914
- #
1915
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1916
- def document_menu_loop
1917
- @menu_base_options = @delegate_object
1918
- @dml_link_state = LinkState.new(
1919
- block_name: @delegate_object[:block_name],
1920
- document_filename: @delegate_object[:filename]
1921
- )
1922
- @run_state.block_name_from_cli = @dml_link_state.block_name.present?
1923
- @cli_block_name = @dml_link_state.block_name
1924
- @dml_now_using_cli = @run_state.block_name_from_cli
1925
- @dml_menu_default_dname = nil
1926
- @dml_block_state = SelectedBlockMenuState.new
1927
-
1928
- @run_state.batch_random = Random.new.rand
1929
- @run_state.batch_index = 0
1930
-
1931
- InputSequencer.new(
1932
- @delegate_object[:filename],
1933
- @delegate_object[:input_cli_rest]
1934
- ).run do |msg, data|
1935
- case msg
1936
- when :parse_document # once for each menu
1937
- # puts "@ - parse document #{data}"
1938
- ii_parse_document(data)
1939
-
1940
- when :display_menu
1941
- # warn "@ - display menu:"
1942
- # ii_display_menu
1943
- @dml_block_state = SelectedBlockMenuState.new
1944
- @delegate_object[:block_name] = nil
1945
-
1946
- when :user_choice
1947
- if @dml_link_state.block_name.present?
1948
- # @prior_block_was_link = true
1949
- @dml_block_state.block = @dml_blocks_in_file.find do |item|
1950
- item[:oname] == @dml_link_state.block_name
1951
- end
1952
- @dml_link_state.block_name = nil
1953
- else
1954
- # puts "? - Select a block to execute (or type #{$texit} to exit):"
1955
- break if ii_user_choice == :break # into @dml_block_state
1956
- break if @dml_block_state.block.nil? # no block matched
1957
- end
1958
- # puts "! - Executing block: #{data}"
1959
- # @dml_block_state.block[:oname]
1960
- @dml_block_state.block&.fetch(:oname, nil)
1961
-
1962
- when :execute_block
1963
- block_name = data
1964
- if block_name == '* Back' ####
1965
- debounce_reset
1966
- @menu_user_clicked_back_link = true
1967
- load_file_link_state = pop_link_history_and_trigger_load
1968
- @dml_link_state = load_file_link_state.link_state
1969
-
1970
- InputSequencer.merge_link_state(
1971
- @dml_link_state,
1972
- InputSequencer.next_link_state(
1973
- block_name: @dml_link_state.block_name,
1974
- document_filename: @dml_link_state.document_filename,
1975
- prior_block_was_link: true
1976
- )
1977
- )
1978
-
1979
- else
1980
- @dml_block_state = block_state_for_name_from_cli(block_name)
1981
- if @dml_block_state.block[:shell] == BlockType::OPTS
1982
- debounce_reset
1983
- link_state = LinkState.new
1984
- options_state = read_show_options_and_trigger_reuse(
1985
- selected: @dml_block_state.block,
1986
- link_state: link_state
1987
- )
1988
-
1989
- @menu_base_options.merge!(options_state.options)
1990
- @delegate_object.merge!(options_state.options)
1991
- options_state.load_file_link_state.link_state
1992
- else
1993
- ii_execute_block(block_name)
1994
-
1995
- if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1996
- selected: @dml_block_state.block)
1997
- return :break
1998
- end
1999
-
2000
- ## order of block name processing: link block, cli, from user
2001
- #
2002
- @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
2003
- HashDelegator.next_link_state(
2004
- block_name: @dml_link_state.block_name,
2005
- block_name_from_cli: !@dml_link_state.block_name.present?,
2006
- block_state: @dml_block_state,
2007
- was_using_cli: @dml_now_using_cli
2008
- )
2009
-
2010
- if !@dml_block_state.block[:block_name_from_ui] && cli_break
2011
- # &bsp '!block_name_from_ui + cli_break -> break'
2012
- return :break
2013
- end
2014
-
2015
- InputSequencer.next_link_state(
2016
- block_name: @dml_link_state.block_name,
2017
- prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
2018
- )
2019
- end
2020
- end
2021
-
2022
- when :exit?
2023
- data == $texit
2024
- when :stay?
2025
- data == $stay
2026
- else
2027
- raise "Invalid message: #{msg}"
2028
- end
2029
- end
2030
- rescue StandardError
2031
- HashDelegator.error_handler('document_menu_loop',
2032
- { abort: true })
2033
- end
2034
-
2035
- def ii_parse_document(_document_filename)
2036
- @run_state.batch_index += 1
2037
- @run_state.in_own_window = false
2038
-
2039
- # &bsp 'loop', block_name_from_cli, @cli_block_name
2040
- @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
2041
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
2042
- now_using_cli: @dml_now_using_cli,
2043
- link_state: @dml_link_state)
2044
- end
2045
-
2046
- def ii_user_choice
2047
- @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
2048
- menu_blocks: @dml_menu_blocks,
2049
- default: @dml_menu_default_dname)
2050
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
2051
- if !@dml_block_state
2052
- HashDelegator.error_handler('block_state missing', { abort: true })
2053
- elsif @dml_block_state.state == MenuState::EXIT
2054
- # &bsp 'load_cli_or_user_selected_block -> break'
2055
- :break
2056
- end
2057
- end
2058
-
2059
- def ii_execute_block(block_name)
2060
- @dml_block_state = block_state_for_name_from_cli(block_name)
2061
-
2062
- dump_and_warn_block_state(selected: @dml_block_state.block)
2063
- @dml_link_state, @dml_menu_default_dname = \
2064
- exec_bash_next_state(
2065
- selected: @dml_block_state.block,
2066
- mdoc: @dml_mdoc,
2067
- link_state: @dml_link_state,
2068
- block_source: {
2069
- document_filename: @delegate_object[:filename],
2070
- time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
2071
- }
2072
- )
2073
- end
2074
-
2075
- def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
2076
- lfls = execute_shell_type(
2077
- selected: selected,
2078
- mdoc: mdoc,
2079
- link_state: link_state,
2080
- block_source: block_source
2081
- )
2082
-
2083
- # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
2084
- [lfls.link_state,
2085
- lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
2086
- end
2175
+ (link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
2176
+ ENV[key] = value.to_s
2177
+ code_lines.push(assign_key_value_in_bash(key, value))
2178
+ end
2179
+ end
2087
2180
 
2088
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2089
- block_name_from_cli, now_using_cli = \
2090
- manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2091
- now_using_cli: now_using_cli,
2092
- link_state: link_state)
2093
- set_delob_filename_block_name(link_state: link_state,
2094
- block_name_from_cli: block_name_from_cli)
2181
+ ## append blocks loaded, apply LinkKeys::EVAL
2182
+ #
2183
+ if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
2184
+ load_filespec = load_filespec_from_expression(load_expr)
2185
+ code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
2186
+ end
2095
2187
 
2096
- # update @delegate_object and @menu_base_options in auto_load
2188
+ # if an eval link block, evaluate code_lines and return its standard output
2097
2189
  #
2098
- blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2099
- dump_delobj(blocks_in_file, menu_blocks, link_state)
2190
+ if link_block_data.fetch(LinkKeys::EVAL,
2191
+ false) || link_block_data.fetch(LinkKeys::EXEC, false)
2192
+ code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
2193
+ end
2100
2194
 
2101
- [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2102
- end
2195
+ next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
2103
2196
 
2104
- # user prompt to exit if the menu will be displayed again
2105
- #
2106
- def prompt_user_exit(block_name_from_cli:, selected:)
2107
- !block_name_from_cli &&
2108
- selected[:shell] == BlockType::BASH &&
2109
- @delegate_object[:pause_after_script_execution] &&
2110
- prompt_select_continue == MenuState::EXIT
2111
- end
2197
+ if link_block_data[LinkKeys::RETURN]
2198
+ pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
2199
+ dependencies, selected)
2112
2200
 
2113
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
2114
- if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2115
- # &bsp 'pause cli control, allow user to select block'
2116
- block_name_from_cli = false
2117
- now_using_cli = false
2118
- @menu_base_options[:block_name] = \
2119
- @delegate_object[:block_name] = \
2120
- link_state.block_name = \
2121
- @cli_block_name = nil
2201
+ else
2202
+ link_history_push_and_next(
2203
+ curr_block_name: selected[:oname],
2204
+ curr_document_filename: @delegate_object[:filename],
2205
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2206
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2207
+ inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2208
+ next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK,
2209
+ nil) || link_block_data[LinkKeys::BLOCK] || '',
2210
+ next_document_filename: next_document_filename,
2211
+ next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
2212
+ )
2122
2213
  end
2123
-
2124
- @delegate_object = @menu_base_options.dup
2125
- @menu_user_clicked_back_link = false
2126
- [block_name_from_cli, now_using_cli]
2127
2214
  end
2128
2215
 
2129
- # Update the block name in the link state and delegate object.
2130
- #
2131
- # This method updates the block name based on whether it was specified
2132
- # through the CLI or derived from the link state.
2133
- #
2134
- # @param link_state [LinkState] The current link state object.
2135
- # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2136
- def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2137
- @delegate_object[:filename] = link_state.document_filename
2138
- link_state.block_name = @delegate_object[:block_name] =
2139
- block_name_from_cli ? @cli_block_name : link_state.block_name
2216
+ # Handle expression with wildcard characters
2217
+ # allow user to select or enter
2218
+ def puts_gets_oprompt_(filespec)
2219
+ puts format(@delegate_object[:prompt_show_expr_format],
2220
+ { expr: filespec })
2221
+ puts @delegate_object[:prompt_enter_filespec]
2222
+ gets.chomp
2140
2223
  end
2141
2224
 
2142
- # Outputs warnings based on the delegate object's configuration
2143
- #
2144
- # @param delegate_object [Hash] The delegate object containing configuration flags.
2145
- # @param blocks_in_file [Hash] Hash of blocks present in the file.
2146
- # @param menu_blocks [Hash] Hash of menu blocks.
2147
- # @param link_state [LinkState] Current state of the link.
2148
- def dump_delobj(blocks_in_file, menu_blocks, link_state)
2149
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
2225
+ # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2226
+ # @param selected [Hash] Selected item from the menu containing a YAML body.
2227
+ # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2228
+ # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2229
+ def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2230
+ obj = {}
2231
+ data = YAML.load(selected[:body].join("\n"))
2232
+ (data || []).each do |key, value|
2233
+ sym_key = key.to_sym
2234
+ obj[sym_key] = value
2150
2235
 
2151
- if @delegate_object[:dump_blocks_in_file]
2152
- warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
2153
- label: 'blocks_in_file')
2236
+ print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2154
2237
  end
2155
2238
 
2156
- if @delegate_object[:dump_menu_blocks]
2157
- warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
2158
- label: 'menu_blocks')
2239
+ link_state.block_name = nil
2240
+ OpenStruct.new(options: obj,
2241
+ load_file_link_state: LoadFileLinkState.new(
2242
+ LoadFile::REUSE, link_state
2243
+ ))
2244
+ end
2245
+
2246
+ # Check if the delegate object responds to a given method.
2247
+ # @param method_name [Symbol] The name of the method to check.
2248
+ # @param include_private [Boolean] Whether to include private methods in the check.
2249
+ # @return [Boolean] true if the delegate object responds to the method, false otherwise.
2250
+ def respond_to?(method_name, include_private = false)
2251
+ if super
2252
+ true
2253
+ elsif @delegate_object.respond_to?(method_name, include_private)
2254
+ true
2255
+ elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
2256
+ true
2257
+ else
2258
+ @delegate_object.respond_to?(method_name, include_private)
2159
2259
  end
2260
+ end
2160
2261
 
2161
- warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
2162
- warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
2163
- return unless @delegate_object[:dump_inherited_lines]
2262
+ def runtime_exception(exception_sym, name, items)
2263
+ if @delegate_object[exception_sym] != 0
2264
+ data = { name: name, detail: items.join(', ') }
2265
+ warn(
2266
+ format(
2267
+ @delegate_object.fetch(:exception_format_name, "\n%{name}"),
2268
+ data
2269
+ ).send(@delegate_object.fetch(:exception_color_name, :red)) +
2270
+ format(
2271
+ @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
2272
+ data
2273
+ ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
2274
+ )
2275
+ end
2276
+ return unless (@delegate_object[exception_sym]).positive?
2164
2277
 
2165
- warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
2278
+ exit @delegate_object[exception_sym]
2166
2279
  end
2167
2280
 
2168
- def dump_and_warn_block_state(selected:)
2169
- if selected.nil?
2170
- Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
2171
- { abort: true })
2281
+ # allow user to select or enter
2282
+ def save_filespec_from_expression(expression)
2283
+ # Process expression with embedded formatting
2284
+ formatted = formatted_expression(expression)
2285
+
2286
+ # Handle wildcards or direct file specification
2287
+ if contains_wildcards?(formatted)
2288
+ save_filespec_wildcard_expansion(formatted)
2289
+ else
2290
+ formatted
2172
2291
  end
2292
+ end
2173
2293
 
2174
- return unless @delegate_object[:dump_selected_block]
2294
+ # Handle expression with wildcard characters
2295
+ # allow user to select or enter
2296
+ def save_filespec_wildcard_expansion(filespec)
2297
+ files = find_files(filespec)
2298
+ case files.count
2299
+ when 0
2300
+ prompt_for_filespec_with_wildcard(filespec)
2301
+ else
2302
+ ## user selects from existing files or other
2303
+ # input into path with wildcard for easy entry
2304
+ #
2305
+ name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
2306
+ case name
2307
+ when @delegate_object[:prompt_filespec_back]
2308
+ # do nothing
2309
+ when @delegate_object[:prompt_filespec_other]
2310
+ prompt_for_filespec_with_wildcard(filespec)
2311
+ else
2312
+ name
2313
+ end
2314
+ end
2315
+ end
2175
2316
 
2176
- warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
2317
+ def save_to_file(required_lines:, selected:)
2318
+ write_command_file(required_lines: required_lines, selected: selected)
2319
+ @fout.fout "File saved: #{@run_state.saved_filespec}"
2177
2320
  end
2178
2321
 
2179
2322
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
@@ -2189,20 +2332,20 @@ module MarkdownExec
2189
2332
  selection = @prompt.select(prompt_text,
2190
2333
  names,
2191
2334
  opts.merge(filter: true))
2192
- item = names.find do |item|
2335
+ selected_name = names.find do |item|
2193
2336
  if item.instance_of?(Hash)
2194
2337
  item[:dname] == selection
2195
2338
  else
2196
2339
  item == selection
2197
2340
  end
2198
2341
  end
2199
- item = { dname: item } if item.instance_of?(String)
2200
- unless item
2342
+ selected_name = { dname: selected_name } if selected_name.instance_of?(String)
2343
+ unless selected_name
2201
2344
  HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
2202
2345
  exit 1
2203
2346
  end
2204
2347
 
2205
- item.merge(
2348
+ selected_name.merge(
2206
2349
  if selection == menu_chrome_colored_option(:menu_option_back_name)
2207
2350
  { option: selection, shell: BlockType::LINK }
2208
2351
  elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
@@ -2217,6 +2360,35 @@ module MarkdownExec
2217
2360
  HashDelegator.error_handler('select_option_with_metadata')
2218
2361
  end
2219
2362
 
2363
+ # Update the block name in the link state and delegate object.
2364
+ #
2365
+ # This method updates the block name based on whether it was specified
2366
+ # through the CLI or derived from the link state.
2367
+ #
2368
+ # @param link_state [LinkState] The current link state object.
2369
+ # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2370
+ def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2371
+ @delegate_object[:filename] = link_state.document_filename
2372
+ link_state.block_name = @delegate_object[:block_name] =
2373
+ block_name_from_cli ? @cli_block_name : link_state.block_name
2374
+ end
2375
+
2376
+ def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2377
+ block_name_from_cli, now_using_cli = \
2378
+ manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2379
+ now_using_cli: now_using_cli,
2380
+ link_state: link_state)
2381
+ set_delob_filename_block_name(link_state: link_state,
2382
+ block_name_from_cli: block_name_from_cli)
2383
+
2384
+ # update @delegate_object and @menu_base_options in auto_load
2385
+ #
2386
+ blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2387
+ dump_delobj(blocks_in_file, menu_blocks, link_state)
2388
+
2389
+ [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2390
+ end
2391
+
2220
2392
  def set_environment_variables_for_block(selected)
2221
2393
  code_lines = []
2222
2394
  YAML.load(selected[:body].join("\n"))&.each do |key, value|
@@ -2352,27 +2524,6 @@ module MarkdownExec
2352
2524
  end
2353
2525
  end
2354
2526
 
2355
- # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2356
- # @param selected [Hash] Selected item from the menu containing a YAML body.
2357
- # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2358
- # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2359
- def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2360
- obj = {}
2361
- data = YAML.load(selected[:body].join("\n"))
2362
- (data || []).each do |key, value|
2363
- sym_key = key.to_sym
2364
- obj[sym_key] = value
2365
-
2366
- print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2367
- end
2368
-
2369
- link_state.block_name = nil
2370
- OpenStruct.new(options: obj,
2371
- load_file_link_state: LoadFileLinkState.new(
2372
- LoadFile::Reuse, link_state
2373
- ))
2374
- end
2375
-
2376
2527
  def wait_for_stream_processing
2377
2528
  @process_mutex.synchronize do
2378
2529
  @process_cv.wait(@process_mutex)
@@ -2447,14 +2598,34 @@ module MarkdownExec
2447
2598
  HashDelegator.error_handler('write_command_file')
2448
2599
  end
2449
2600
 
2601
+ # Ensure the directory exists before writing the file
2602
+ def write_file_with_directory_creation(save_filespec, content)
2603
+ directory = File.dirname(save_filespec)
2604
+
2605
+ begin
2606
+ FileUtils.mkdir_p(directory)
2607
+ File.write(save_filespec, content)
2608
+ rescue Errno::EACCES
2609
+ warn "Permission denied: Unable to write to file '#{save_filespec}'"
2610
+ nil
2611
+ rescue Errno::EROFS
2612
+ warn "Read-only file system: Unable to write to file '#{save_filespec}'"
2613
+ nil
2614
+ rescue StandardError => err
2615
+ warn "An error occurred while writing to file '#{save_filespec}': #{err.message}"
2616
+ nil
2617
+ end
2618
+ end
2619
+
2620
+ # return next document file name
2450
2621
  def write_inherited_lines_to_file(link_state, link_block_data)
2451
- save_expr = link_block_data.fetch(LinkKeys::Save, '')
2622
+ save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
2452
2623
  if save_expr.present?
2453
2624
  save_filespec = save_filespec_from_expression(save_expr)
2454
2625
  File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
2455
2626
  @delegate_object[:filename]
2456
2627
  else
2457
- link_block_data[LinkKeys::File] || @delegate_object[:filename]
2628
+ link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
2458
2629
  end
2459
2630
  end
2460
2631
  end
@@ -2490,21 +2661,6 @@ module MarkdownExec
2490
2661
 
2491
2662
  def self.next_link_state(*args, **kwargs, &block)
2492
2663
  super
2493
- # result = super
2494
-
2495
- # @logger ||= StdOutErrLogger.new
2496
- # @logger.unknown(
2497
- # HashDelegator.clean_hash_recursively(
2498
- # { "HashDelegator.next_link_state":
2499
- # { 'args': args,
2500
- # 'at': Time.now.strftime('%FT%TZ'),
2501
- # 'for': /[^\/]+:\d+/.match(caller.first)[0],
2502
- # 'kwargs': kwargs,
2503
- # 'return': result } }
2504
- # )
2505
- # )
2506
-
2507
- # result
2508
2664
  end
2509
2665
  end
2510
2666
  end
@@ -2595,21 +2751,21 @@ module MarkdownExec
2595
2751
 
2596
2752
  # Test case for empty body
2597
2753
  def test_push_link_history_and_trigger_load_with_empty_body
2598
- assert_equal LoadFile::Reuse,
2754
+ assert_equal LoadFile::REUSE,
2599
2755
  @hd.push_link_history_and_trigger_load.load_file
2600
2756
  end
2601
2757
 
2602
2758
  # Test case for non-empty body without 'file' key
2603
2759
  def test_push_link_history_and_trigger_load_without_file_key
2604
2760
  body = ["vars:\n KEY: VALUE"]
2605
- assert_equal LoadFile::Reuse,
2761
+ assert_equal LoadFile::REUSE,
2606
2762
  @hd.push_link_history_and_trigger_load(link_block_body: body).load_file
2607
2763
  end
2608
2764
 
2609
2765
  # Test case for non-empty body with 'file' key
2610
2766
  def test_push_link_history_and_trigger_load_with_file_key
2611
2767
  body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
2612
- expected_result = LoadFileLinkState.new(LoadFile::Load,
2768
+ expected_result = LoadFileLinkState.new(LoadFile::LOAD,
2613
2769
  LinkState.new(block_name: 'sample_block',
2614
2770
  document_filename: 'sample_file',
2615
2771
  inherited_dependencies: {},
@@ -2647,8 +2803,9 @@ module MarkdownExec
2647
2803
  end
2648
2804
 
2649
2805
  def test_safeval_rescue_from_error
2650
- HashDelegator.stubs(:error_handler).with('safeval')
2651
- assert_nil HashDelegator.safeval('invalid code')
2806
+ assert_raises(SystemExit) do
2807
+ HashDelegator.safeval('invalid_code_raises_exception')
2808
+ end
2652
2809
  end
2653
2810
 
2654
2811
  def test_set_fcb_title
@@ -3054,7 +3211,7 @@ module MarkdownExec
3054
3211
 
3055
3212
  # Asserting the result is an instance of LoadFileLinkState
3056
3213
  assert_instance_of LoadFileLinkState, result
3057
- assert_equal LoadFile::Load, result.load_file
3214
+ assert_equal LoadFile::LOAD, result.load_file
3058
3215
  assert_nil result.link_state.block_name
3059
3216
  end
3060
3217
  end
@@ -3441,4 +3598,60 @@ module MarkdownExec
3441
3598
  assert_equal expected, BashCommentFormatter.format_comment(input)
3442
3599
  end
3443
3600
  end
3601
+
3602
+ class PromptForFilespecWithWildcardTest < Minitest::Test
3603
+ def setup
3604
+ @delegate_object = {
3605
+ prompt_show_expr_format: 'Current expression: %{expr}',
3606
+ prompt_enter_filespec: 'Please enter a filespec:'
3607
+ }
3608
+ @original_stdin = $stdin
3609
+ end
3610
+
3611
+ def teardown
3612
+ $stdin = @original_stdin
3613
+ end
3614
+
3615
+ def test_prompt_for_filespec_with_normal_input
3616
+ $stdin = StringIO.new("test_input\n")
3617
+ result = prompt_for_filespec_with_wildcard('*.txt')
3618
+ assert_equal 'resolved_path_or_substituted_value', result
3619
+ end
3620
+
3621
+ def test_prompt_for_filespec_with_interruption
3622
+ $stdin = StringIO.new
3623
+ # rubocop disable:Lint/NestedMethodDefinition
3624
+ def $stdin.gets; raise Interrupt; end
3625
+ # rubocop enable:Lint/NestedMethodDefinition
3626
+
3627
+ result = prompt_for_filespec_with_wildcard('*.txt')
3628
+ assert_nil result
3629
+ end
3630
+
3631
+ def test_prompt_for_filespec_with_empty_input
3632
+ $stdin = StringIO.new("\n")
3633
+ result = prompt_for_filespec_with_wildcard('*.txt')
3634
+ assert_equal 'resolved_path_or_substituted_value', result
3635
+ end
3636
+
3637
+ private
3638
+
3639
+ def prompt_for_filespec_with_wildcard(filespec)
3640
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
3641
+ puts @delegate_object[:prompt_enter_filespec]
3642
+
3643
+ begin
3644
+ input = gets.chomp
3645
+ PathUtils.resolve_path_or_substitute(input, filespec)
3646
+ rescue Interrupt
3647
+ nil
3648
+ end
3649
+ end
3650
+
3651
+ module PathUtils
3652
+ def self.resolve_path_or_substitute(input, filespec)
3653
+ 'resolved_path_or_substituted_value' # Placeholder implementation
3654
+ end
3655
+ end
3656
+ end
3444
3657
  end # module MarkdownExec