lib/hash_delegator.rb in markdown_exec-1.8.1 vs lib/hash_delegator.rb in markdown_exec-1.8.2

- old
+ new

@@ -143,14 +143,10 @@ add_dividers(menu_blocks) end private - def should_add_back_option? - @delegate_object[:menu_with_back] && history_env_state_exist? - end - def add_back_option(menu_blocks) append_chrome_block(menu_blocks, MenuState::BACK) end def add_exit_option(menu_blocks) @@ -214,46 +210,20 @@ position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider) end # private - def divider_formatting_present?(position) - divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider - @delegate_object[:menu_divider_format].present? && @delegate_object[divider_key].present? - end - - def create_divider(position) - divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider - oname = format(@delegate_object[:menu_divider_format], - safeval(@delegate_object[divider_key])) - - FCB.new( - chrome: true, - disabled: '', - dname: string_send_color(oname, :menu_divider_color), - oname: oname - ) - end - - # Execute a code block after approval and provide user interaction options. + # Applies shell color options to the given string if applicable. # - # This method displays required code blocks, asks for user approval, and - # executes the code block if approved. It also allows users to copy the - # code to the clipboard or save it to a file. - # - # @param opts [Hash] Options hash containing configuration settings. - # @param mdoc [YourMDocClass] An instance of the MDoc class. - # - def approve_and_execute_block(selected, mdoc) - if selected.fetch(:shell, '') == BlockType::LINK - handle_link_block(selected.fetch(:body, ''), mdoc, selected) - elsif @menu_user_clicked_back_link - handle_back_link - elsif selected[:shell] == BlockType::OPTS - handle_opts_block(selected, @menu_base_options) + # @param name [String] The name to potentially colorize. + # @param shell_color_option [Symbol, nil] The shell color option to apply. + # @return [String] The colorized or original name string. + def apply_shell_color_option(name, shell_color_option) + if shell_color_option && @delegate_object[shell_color_option].present? + string_send_color(name, shell_color_option) else - handle_generic_block(mdoc, selected) + name end end # private @@ -284,26 +254,10 @@ error_handler('blocks_from_nested_files') end # private - def process_block_based_on_type(blocks, btype, fcb) - case btype - when :blocks - blocks.push(get_block_summary(fcb)) - when :filter - %i[blocks line] - when :line - unless @delegate_object[:no_chrome] - create_and_add_chrome_blocks(blocks, - fcb) - end - end - end - - # private - def cfile @cfile ||= CachedNestedFileReader.new( import_pattern: @delegate_object.fetch(:import_pattern) #, "^ *@import +(?<name>.+?) *$") ) end @@ -320,37 +274,18 @@ return false end true end - def runtime_exception(exception_sym, name, items) - if @delegate_object[exception_sym] != 0 - data = { name: name, detail: items.join(', ') } - warn( - format( - @delegate_object.fetch(:exception_format_name, "\n%{name}"), - data - ).send(@delegate_object.fetch(:exception_color_name, :red)) + - format( - @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"), - data - ).send(@delegate_object.fetch(:exception_color_detail, :yellow)) - ) - end - return unless (@delegate_object[exception_sym]).positive? - - exit @delegate_object[exception_sym] - end - # Collects required code lines based on the selected block and the delegate object's configuration. # If the block type is VARS, it also sets environment variables based on the block's content. # # @param mdoc [YourMDocClass] An instance of the MDoc class. # @param selected [Hash] The selected block. # @return [Array<String>] Required code blocks as an array of lines. def collect_required_code_lines(mdoc, selected) - set_environment_variables(selected) if selected[:shell] == BlockType::VARS + set_environment_variables_for_block(selected) if selected[:shell] == BlockType::VARS required = mdoc.collect_recursively_required_code( @delegate_object[:block_name], label_format_above: @delegate_object[:shell_code_label_format_above], label_format_below: @delegate_object[:shell_code_label_format_below] @@ -365,28 +300,14 @@ highlight: [@delegate_object[:block_name]]) end read_required_blocks_from_temp_file + required[:code] end - # private - - def set_environment_variables(selected) - YAML.load(selected[:body].join("\n")).each do |key, value| - ENV[key] = value.to_s - next unless @delegate_object[:menu_vars_set_format].present? - - formatted_string = format(@delegate_object[:menu_vars_set_format], - { key: key, value: value }) - print string_send_color(formatted_string, :menu_vars_set_color) - end - end - def command_execute(command, args: []) @run_state.files = Hash.new([]) @run_state.options = @delegate_object @run_state.started_at = Time.now.utc -# rbp Open3.popen3(@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args) do |stdin, stdout, stderr, exec_thr| @@ -440,10 +361,27 @@ SelectedBlockMenuState.new(block, state) rescue StandardError error_handler('command_or_user_selected_block') end + # This method is responsible for handling the execution of generic blocks in a markdown document. + # It collects the required code lines from the document and, depending on the configuration, + # may display the code for user approval before execution. It then executes the approved block. + # + # @param mdoc [Object] The markdown document object containing code blocks. + # @param selected [Hash] The selected item from the menu to be executed. + # @return [LoadFileNextBlock] An object indicating whether to load the next block or reuse the current one. + def compile_execute_bash_and_special_blocks_and_trigger_reuse(mdoc, selected) + required_lines = collect_required_code_lines(mdoc, selected) + output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve] + display_required_code(required_lines) if output_or_approval + allow_execution = @delegate_object[:user_must_approve] ? prompt_for_user_approval(required_lines) : true + execute_approved_block(required_lines, selected) if allow_execution + + LoadFileNextBlock.new(LoadFile::Reuse, '') + end + def copy_to_clipboard(required_lines) text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR) Clipboard.copy(text) @fout.fout "Clipboard updated: #{required_lines.count} blocks," \ " #{required_lines.flatten.count} lines," \ @@ -533,16 +471,21 @@ def create_directory_for_file(file_path) FileUtils.mkdir_p(File.dirname(file_path)) end - def write_file_content(file_path, content) - File.write(file_path, content) - end + def create_divider(position) + divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider + oname = format(@delegate_object[:menu_divider_format], + safeval(@delegate_object[divider_key])) - def set_file_permissions(file_path, chmod_value) - File.chmod(chmod_value, file_path) + FCB.new( + chrome: true, + disabled: '', + dname: string_send_color(oname, :menu_divider_color), + oname: oname + ) end # Creates a temporary file, writes the provided code blocks into it, # and sets an environment variable with the file path. # @param code_blocks [String] Code blocks to write into the file. @@ -556,18 +499,10 @@ def create_temp_file Dir::Tmpname.create(self.class.to_s) { |path| path } end - def write_to_file(path, content) - File.write(path, content) - end - - def set_environment_variable(path) - ENV['MDE_LINK_REQUIRED_FILE'] = path - end - # Updates the title of an FCB object from its body content if the title is nil or empty. def default_block_title_from_body(fcb) return unless fcb.title.nil? || fcb.title.empty? fcb.derive_title_from_body @@ -593,20 +528,10 @@ clear_required_file rescue StandardError error_handler('delete_required_temp_file') end - # private - - def fetch_temp_blocks_file_path - ENV.fetch('MDE_LINK_REQUIRED_FILE', nil) - end - - def safely_remove_file(path) - FileUtils.rm_f(path) - end - # Determines the state of a selected block in the menu based on the selected option. # It categorizes the selected option into either EXIT, BACK, or CONTINUE state. # # @param selected_option [Hash] The selected menu option. # @return [SelectedBlockMenuState] An object representing the state of the selected block. @@ -634,16 +559,13 @@ required_lines.each { |cb| @fout.fout cb } output_color_formatted(:script_preview_tail, :script_preview_frame_color) end - # private - - def output_color_formatted(data_sym, color_sym) - formatted_string = string_send_color(@delegate_object[data_sym], - color_sym) - @fout.fout formatted_string + def divider_formatting_present?(position) + divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider + @delegate_object[:menu_divider_format].present? && @delegate_object[divider_key].present? end def error_handler(name = '', opts = {}) Exceptions.error_handler( "HashDelegator.#{name} -- #{$!}", @@ -664,35 +586,31 @@ write_command_file_if_needed(required_lines) format_and_execute_command(required_lines) post_execution_process end - # private - - def set_script_block_name(selected) - @run_state.script_block_name = selected[:oname] + # Execute a code block after approval and provide user interaction options. + # + # This method displays required code blocks, asks for user approval, and + # executes the code block if approved. It also allows users to copy the + # code to the clipboard or save it to a file. + # + # @param opts [Hash] Options hash containing configuration settings. + # @param mdoc [YourMDocClass] An instance of the MDoc class. + # + def execute_bash_and_special_blocks(selected, mdoc) + if selected.fetch(:shell, '') == BlockType::LINK + push_link_history_and_trigger_load(selected.fetch(:body, ''), mdoc, selected) + elsif @menu_user_clicked_back_link + pop_link_history_and_trigger_load + elsif selected[:shell] == BlockType::OPTS + update_options_and_trigger_reuse(selected, @menu_base_options) + else + compile_execute_bash_and_special_blocks_and_trigger_reuse(mdoc, selected) + end end - def write_command_file_if_needed(lines) - write_command_file(lines) if @delegate_object[:save_executed_script] - end - - def format_and_execute_command(lines) - formatted_command = lines.flatten.join("\n") - @fout.fout fetch_color(data_sym: :script_execution_head, - color_sym: :script_execution_frame_color) - command_execute(formatted_command, args: @pass_args) - @fout.fout fetch_color(data_sym: :script_execution_tail, - color_sym: :script_execution_frame_color) - end - - def post_execution_process - initialize_and_save_execution_output - output_execution_summary - output_execution_result - end - # Retrieves a specific data symbol from the delegate object, converts it to a string, # and applies a color style based on the specified color symbol. # # @param default [String] The default value if the data symbol is not found. # @param data_sym [Symbol] The symbol key to fetch data from the delegate object. @@ -703,10 +621,35 @@ color_sym: :execution_report_preview_frame_color) data_string = @delegate_object.fetch(data_sym, default).to_s string_send_color(data_string, color_sym) end + def fetch_temp_blocks_file_path + ENV.fetch('MDE_LINK_REQUIRED_FILE', nil) + end + + # DebugHelper.d ["HDmm method_name: #{method_name}", "#{first_n_caller_items 1}"] + def first_n_caller_items(n) + # Get the call stack + call_stack = caller + base_path = File.realpath('.') + + # Modify the call stack to remove the base path and keep only the first n items + call_stack.take(n + 1)[1..].map do |line| + " . #{line.sub(/^#{Regexp.escape(base_path)}\//, '')}" + end.join("\n") + end + + def format_and_execute_command(lines) + formatted_command = lines.flatten.join("\n") + @fout.fout fetch_color(data_sym: :script_execution_head, + color_sym: :script_execution_frame_color) + command_execute(formatted_command, args: @pass_args) + @fout.fout fetch_color(data_sym: :script_execution_tail, + color_sym: :script_execution_frame_color) + end + # Formats a string based on a given context and applies color styling to it. # It retrieves format and color information from the delegate object and processes accordingly. # # @param default [String] The default value if the format symbol is not found (unused in current implementation). # @param context [Hash] Contextual data used for string formatting. @@ -754,35 +697,10 @@ fcb.dname = apply_shell_color_option(fcb.oname, shell_color_option) fcb end - # private - - # Applies shell color options to the given string if applicable. - # - # @param name [String] The name to potentially colorize. - # @param shell_color_option [Symbol, nil] The shell color option to apply. - # @return [String] The colorized or original name string. - def apply_shell_color_option(name, shell_color_option) - if shell_color_option && @delegate_object[shell_color_option].present? - string_send_color(name, shell_color_option) - else - name - end - end - - # This method handles the back-link operation in the Markdown execution context. - # It updates the history state and prepares to load the next block. - # - # @return [LoadFileNextBlock] An object indicating the action to load the next block. - def handle_back_link - history_state_pop - LoadFileNextBlock.new(LoadFile::Load, '') - end - - # private # Updates the delegate object's state based on the provided block state. # It sets the block name and determines if the user clicked the back link in the menu. # # @param block_state [Object] An object representing the state of a block in the menu. def handle_block_state(block_state) @@ -790,92 +708,13 @@ MenuState::CONTINUE].include?(block_state.state) return end @delegate_object[:block_name] = block_state.block[:oname] -# rbp @menu_user_clicked_back_link = block_state.state == MenuState::BACK end - # This method is responsible for handling the execution of generic blocks in a markdown document. - # It collects the required code lines from the document and, depending on the configuration, - # may display the code for user approval before execution. It then executes the approved block. - # - # @param mdoc [Object] The markdown document object containing code blocks. - # @param selected [Hash] The selected item from the menu to be executed. - # @return [LoadFileNextBlock] An object indicating whether to load the next block or reuse the current one. - def handle_generic_block(mdoc, selected) -# rbp - required_lines = collect_required_code_lines(mdoc, selected) - output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve] - display_required_code(required_lines) if output_or_approval - allow_execution = @delegate_object[:user_must_approve] ? prompt_for_user_approval(required_lines) : true - execute_approved_block(required_lines, selected) if allow_execution - - LoadFileNextBlock.new(LoadFile::Reuse, '') - end - - # Handles the processing of a link block in Markdown Execution. - # It loads YAML data from the body content, pushes the state to history, - # sets environment variables, and decides on the next block to load. - # - # @param body [Array<String>] The body content as an array of strings. - # @param mdoc [Object] Markdown document object. - # @param selected [Boolean] Selected state. - # @return [LoadFileNextBlock] Object indicating the next action for file loading. - def handle_link_block(body, mdoc, selected) - data = parse_yaml_data_from_body(body) - data_file = data['file'] - return LoadFileNextBlock.new(LoadFile::Reuse, '') unless data_file - - history_state_push(mdoc, data_file, selected) - set_environment_variables(data['vars']) - - LoadFileNextBlock.new(LoadFile::Load, data['block'] || '') - end - - # private - - def parse_yaml_data_from_body(body) - body.any? ? YAML.load(body.join("\n")) : {} - end - - def set_environment_variables(vars) - vars ||= [] - vars.each { |key, value| ENV[key] = value.to_s } - end - - # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output. - # @param selected [Hash] Selected item from the menu containing a YAML body. - # @param tgt2 [Hash, nil] An optional target hash to update with YAML data. - # @return [LoadFileNextBlock] An instance indicating the next action for loading files. - def handle_opts_block(selected, tgt2 = nil) - data = YAML.load(selected[:body].join("\n")) - (data || []).each do |key, value| - update_delegate_and_target(key, value, tgt2) - if @delegate_object[:menu_opts_set_format].present? - print_formatted_option(key, - value) - end - end - LoadFileNextBlock.new(LoadFile::Reuse, '') - end - - # private - - def update_delegate_and_target(key, value, tgt2) - sym_key = key.to_sym - @delegate_object[sym_key] = value - tgt2[sym_key] = value if tgt2 - end - - def print_formatted_option(key, value) - formatted_str = format(@delegate_object[:menu_opts_set_format], - { key: key, value: value }) - print string_send_color(formatted_str, :menu_opts_set_color) - end - def handle_stream(stream, file_type, swap: false) @process_mutex.synchronize do Thread.new do stream.each_line do |line| line.strip! @@ -894,14 +733,14 @@ @process_cv.signal end end end - def wait_for_stream_processing - @process_mutex.synchronize do - @process_cv.wait(@process_mutex) - end + # Checks if a history environment variable is set and returns its value if present. + # @return [String, nil] The value of the history environment variable or nil if not present. + def history_env_state_exist? + ENV.fetch(MDE_HISTORY_ENV_NAME, '').present? end # Partitions the history state from the environment variable based on the document separator. # @return [Hash] A hash containing two parts: :unit (first part) and :rest (remaining part). def history_state_partition @@ -950,14 +789,10 @@ return body unless indent&.non_empty? body.lines.map { |line| indent + line.chomp }.join("\n") end - def initialize_fcb_names(fcb) - fcb.oname = fcb.dname = fcb.title || '' - end - # Initializes variables for regex and other states def initial_state { fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch( :fenced_start_and_end_regex, '^(?<indent> *)`{3,}' @@ -987,10 +822,14 @@ @delegate_object[:logged_stdout_filename] @logged_stdout_filespec = @delegate_object[:logged_stdout_filespec] write_execution_output_to_file end + def initialize_fcb_names(fcb) + fcb.oname = fcb.dname = fcb.title || '' + end + # Iterates through blocks in a file, applying the provided block to each line. # The iteration only occurs if the file exists. # @yield [Symbol] :filter Yields to obtain selected messages for processing. def iter_blocks_from_nested_files(&block) return unless check_file_existence(@delegate_object[:filename]) @@ -1017,50 +856,15 @@ end block = block_find(all_blocks, :oname, block_name) return unless block - handle_opts_block(block, @delegate_object) + update_options_and_trigger_reuse(block, @delegate_object) @most_recent_loaded_filename = @delegate_object[:filename] true end - # DebugHelper.d ["HDmm method_name: #{method_name}", "#{first_n_caller_items 1}"] - def first_n_caller_items(n) - # Get the call stack - call_stack = caller - base_path = File.realpath('.') - - # Modify the call stack to remove the base path and keep only the first n items - call_stack.take(n + 1)[1..].map do |line| - " . #{line.sub(/^#{Regexp.escape(base_path)}\//, '')}" - end.join("\n") - end - - # Checks if a history environment variable is set and returns its value if present. - # @return [String, nil] The value of the history environment variable or nil if not present. - def history_env_state_exist? - ENV.fetch(MDE_HISTORY_ENV_NAME, '').present? - end - - # def history_env_state_exist? - # history = ENV.fetch(MDE_HISTORY_ENV_NAME, '') - # history.present? ? history : nil - # end - - # If a method is missing, treat it as a key for the @delegate_object. - def method_missing(method_name, *args, &block) - if @delegate_object.respond_to?(method_name) - @delegate_object.send(method_name, *args, &block) - elsif method_name.to_s.end_with?('=') && args.size == 1 - @delegate_object[method_name.to_s.chop.to_sym] = args.first - else - @delegate_object[method_name] - # super - end - end - def mdoc_and_blocks_from_nested_files menu_blocks = blocks_from_nested_files mdoc = MDoc.new(menu_blocks) do |nopts| @delegate_object.merge!(nopts) end @@ -1072,13 +876,11 @@ def mdoc_menu_and_blocks_from_nested_files all_blocks, mdoc = mdoc_and_blocks_from_nested_files # recreate menu with new options # - if load_auto_blocks(all_blocks) - all_blocks, mdoc = mdoc_and_blocks_from_nested_files - end + all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_blocks(all_blocks) menu_blocks = mdoc.fcbs_per_options(@delegate_object) add_menu_chrome_blocks!(menu_blocks) delete_consecutive_blank_lines!(menu_blocks) if true ### compress empty lines [all_blocks, menu_blocks, mdoc] @@ -1106,18 +908,37 @@ else option_value end end + # If a method is missing, treat it as a key for the @delegate_object. + def method_missing(method_name, *args, &block) + if @delegate_object.respond_to?(method_name) + @delegate_object.send(method_name, *args, &block) + elsif method_name.to_s.end_with?('=') && args.size == 1 + @delegate_object[method_name.to_s.chop.to_sym] = args.first + else + @delegate_object[method_name] + # super + end + end + def next_block_name_from_command_line_arguments return MenuControl::Repeat unless @delegate_object[:input_cli_rest].present? -# rbp @delegate_object[:block_name] = @delegate_object[:input_cli_rest].pop MenuControl::Fresh end + # private + + def output_color_formatted(data_sym, color_sym) + formatted_string = string_send_color(@delegate_object[data_sym], + color_sym) + @fout.fout formatted_string + end + def output_execution_result @fout.fout fetch_color(data_sym: :execution_report_preview_head, color_sym: :execution_report_preview_frame_color) [ ['Block', @run_state.script_block_name], @@ -1160,10 +981,31 @@ :output_execution_label_value_color) }, format_sym: :output_execution_label_format ), level: level end + # private + + def parse_yaml_data_from_body(body) + body.any? ? YAML.load(body.join("\n")) : {} + end + + # This method handles the back-link operation in the Markdown execution context. + # It updates the history state and prepares to load the next block. + # + # @return [LoadFileNextBlock] An object indicating the action to load the next block. + def pop_link_history_and_trigger_load + history_state_pop + LoadFileNextBlock.new(LoadFile::Load, '') + end + + def post_execution_process + initialize_and_save_execution_output + output_execution_summary + output_execution_result + end + # Prepare the blocks menu by adding labels and other necessary details. # # @param all_blocks [Array<Hash>] The list of blocks from the file. # @param opts [Hash] The options hash. # @return [Array<Hash>] The updated blocks menu. @@ -1187,10 +1029,32 @@ ) fcb.to_h end.compact end + def print_formatted_option(key, value) + formatted_str = format(@delegate_object[:menu_opts_set_format], + { key: key, value: value }) + print string_send_color(formatted_str, :menu_opts_set_color) + end + + # private + + def process_block_based_on_type(blocks, btype, fcb) + case btype + when :blocks + blocks.push(get_block_summary(fcb)) + when :filter + %i[blocks line] + when :line + unless @delegate_object[:no_chrome] + create_and_add_chrome_blocks(blocks, + fcb) + end + end + end + ## # Presents a menu to the user for approving an action and performs additional tasks based on the selection. # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file. # # @param opts [Hash] A hash containing various options for the menu. @@ -1245,17 +1109,31 @@ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE rescue TTY::Reader::InputInterrupt exit 1 end - def save_to_file(required_lines) - write_command_file(required_lines) - @fout.fout "File saved: #{@run_state.saved_filespec}" - end - # public + # Handles the processing of a link block in Markdown Execution. + # It loads YAML data from the body content, pushes the state to history, + # sets environment variables, and decides on the next block to load. + # + # @param body [Array<String>] The body content as an array of strings. + # @param mdoc [Object] Markdown document object. + # @param selected [Boolean] Selected state. + # @return [LoadFileNextBlock] Object indicating the next action for file loading. + def push_link_history_and_trigger_load(body, mdoc, selected) + data = parse_yaml_data_from_body(body) + data_file = data['file'] + return LoadFileNextBlock.new(LoadFile::Reuse, '') unless data_file + + history_state_push(mdoc, data_file, selected) + set_environment_variables_per_array(data['vars']) + + LoadFileNextBlock.new(LoadFile::Load, data['block'] || '') + end + # Reads required code blocks from a temporary file specified by an environment variable. # @return [Array<String>] Lines read from the temporary file, or an empty array if file is not found or path is empty. def read_required_blocks_from_temp_file temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil) return [] if temp_blocks_file_path.to_s.empty? @@ -1267,10 +1145,33 @@ else [] end end + def runtime_exception(exception_sym, name, items) + if @delegate_object[exception_sym] != 0 + data = { name: name, detail: items.join(', ') } + warn( + format( + @delegate_object.fetch(:exception_format_name, "\n%{name}"), + data + ).send(@delegate_object.fetch(:exception_color_name, :red)) + + format( + @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"), + data + ).send(@delegate_object.fetch(:exception_color_detail, :yellow)) + ) + end + return unless (@delegate_object[exception_sym]).positive? + + exit @delegate_object[exception_sym] + end + + def safely_remove_file(path) + FileUtils.rm_f(path) + end + # Evaluates the given string as Ruby code and rescues any StandardErrors. # If an error occurs, it calls the error_handler method with 'safeval'. # @param str [String] The string to be evaluated. # @return [Object] The result of evaluating the string. def safeval(str) @@ -1283,30 +1184,34 @@ # eval(str) # rescue StandardError # error_handler('safeval') # end + def save_to_file(required_lines) + write_command_file(required_lines) + @fout.fout "File saved: #{@run_state.saved_filespec}" + end + # Select and execute a code block from a Markdown document. # # This method allows the user to interactively select a code block from a # Markdown document, obtain approval, and execute the chosen block of code. # # @return [Nil] Returns nil if no code block is selected or an error occurs. - def select_approve_and_execute_block(_execute: true) + def select_execute_bash_and_special_blocks(_execute: true) @menu_base_options = @delegate_object repeat_menu = @menu_base_options[:block_name].present? ? MenuControl::Fresh : MenuControl::Repeat load_file_next_block = LoadFileNextBlock.new(LoadFile::Reuse) - default = nil + menu_default_dname = nil @menu_state_filename = @menu_base_options[:filename] @menu_state_block_name = @menu_base_options[:block_name] loop do loop do @delegate_object = @menu_base_options.dup @menu_base_options[:filename] = @menu_state_filename -# rbp @menu_base_options[:block_name] = @menu_state_block_name @menu_state_filename = nil @menu_state_block_name = nil @menu_user_clicked_back_link = false @@ -1322,28 +1227,30 @@ compact_and_index_hash(menu_blocks), label: 'menu_blocks' ) end block_state = command_or_user_selected_block(blocks_in_file, - menu_blocks, default) + menu_blocks, menu_default_dname) return if block_state.state == MenuState::EXIT if block_state.block.nil? - warn_format('select_approve_and_execute_block', "Block not found -- #{@delegate_object[:block_name]}", + warn_format('select_execute_bash_and_special_blocks', "Block not found -- #{@delegate_object[:block_name]}", { abort: true }) # error_handler("Block not found -- #{opts[:block_name]}", { abort: true }) end if @delegate_object[:dump_selected_block] warn block_state.block.to_yaml.sub(/^(?:---\n)?/, "Block:\n") end -# rbp - load_file_next_block = approve_and_execute_block(block_state.block, - mdoc) - default = load_file_next_block.load_file == LoadFile::Load ? nil : @delegate_object[:block_name] -# rbp + load_file_next_block = execute_bash_and_special_blocks(block_state.block, + mdoc) + # if the same menu is being displayed, + # collect the display name of the selected menu item + # for use as the default item + menu_default_dname = load_file_next_block.load_file == LoadFile::Load ? nil : block_state.block[:dname] + @menu_base_options[:block_name] = @delegate_object[:block_name] = load_file_next_block.next_block @menu_base_options[:filename] = @delegate_object[:filename] # user prompt to exit if the menu will be displayed again @@ -1367,11 +1274,11 @@ repeat_menu = next_block_name_from_command_line_arguments @menu_state_filename = @menu_base_options[:filename] @menu_state_block_name = @menu_base_options[:block_name] end rescue StandardError - error_handler('select_approve_and_execute_block', + error_handler('select_execute_bash_and_special_blocks', { abort: true }) end # Presents a TTY prompt to select an option or exit, returns metadata including option and selected def select_option_with_metadata(prompt_text, names, opts = {}) @@ -1398,10 +1305,42 @@ exit 1 rescue StandardError error_handler('select_option_with_metadata') end + def set_environment_variable(path) + ENV['MDE_LINK_REQUIRED_FILE'] = path + end + + def set_environment_variables_for_block(selected) + YAML.load(selected[:body].join("\n")).each do |key, value| + ENV[key] = value.to_s + next unless @delegate_object[:menu_vars_set_format].present? + + formatted_string = format(@delegate_object[:menu_vars_set_format], + { key: key, value: value }) + print string_send_color(formatted_string, :menu_vars_set_color) + end + end + + def set_environment_variables_per_array(vars) + vars ||= [] + vars.each { |key, value| ENV[key] = value.to_s } + end + + def set_file_permissions(file_path, chmod_value) + File.chmod(chmod_value, file_path) + end + + def set_script_block_name(selected) + @run_state.script_block_name = selected[:oname] + end + + def should_add_back_option? + @delegate_object[:menu_with_back] && history_env_state_exist? + end + # Initializes a new fenced code block (FCB) object based on the provided line and heading information. # @param line [String] The line initiating the fenced block. # @param headings [Array<String>] Current headings hierarchy. # @param fenced_start_extended_regex [Regexp] Regular expression to identify fenced block start. # @return [MarkdownExec::FCB] A new FCB instance with the parsed attributes. @@ -1456,11 +1395,18 @@ raise TTY::Reader::InputInterrupt }, symbols: { cross: ' ' } ) end + # private + def update_delegate_and_target(key, value, tgt2) + sym_key = key.to_sym + @delegate_object[sym_key] = value + tgt2[sym_key] = value if tgt2 + end + # Updates the hierarchy of document headings based on the given line. # Utilizes regular expressions to identify heading levels. # @param line [String] The line of text to check for headings. # @param headings [Array<String>] Current headings hierarchy. # @return [Array<String>] Updated headings hierarchy. @@ -1553,10 +1499,29 @@ default_block_title_from_body(fcb) yield_to_block_if_applicable(fcb, selected_messages, &block) end + # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output. + # @param selected [Hash] Selected item from the menu containing a YAML body. + # @param tgt2 [Hash, nil] An optional target hash to update with YAML data. + # @return [LoadFileNextBlock] An instance indicating the next action for loading files. + def update_options_and_trigger_reuse(selected, tgt2 = nil) + data = YAML.load(selected[:body].join("\n")) + (data || []).each do |key, value| + update_delegate_and_target(key, value, tgt2) + print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present? + end + LoadFileNextBlock.new(LoadFile::Reuse, '') + end + + def wait_for_stream_processing + @process_mutex.synchronize do + @process_cv.wait(@process_mutex) + end + end + def wait_for_user_selected_block(all_blocks, menu_blocks, default) block_state = wait_for_user_selection(all_blocks, menu_blocks, default) handle_block_state(block_state) block_state @@ -1588,11 +1553,10 @@ # Handles the core logic for generating the command file's metadata and content. def write_command_file(required_lines) return unless @delegate_object[:save_executed_script] -# rbp time_now = Time.now.utc @run_state.saved_script_filename = SavedAsset.script_name(blockname: @delegate_object[:block_name], filename: @delegate_object[:filename], prefix: @delegate_object[:saved_script_filename_prefix], @@ -1620,10 +1584,14 @@ ) rescue StandardError error_handler('write_command_file') end + def write_command_file_if_needed(lines) + write_command_file(lines) if @delegate_object[:save_executed_script] + end + def write_execution_output_to_file FileUtils.mkdir_p File.dirname(@delegate_object[:logged_stdout_filespec]) File.write( @delegate_object[:logged_stdout_filespec], @@ -1635,10 +1603,14 @@ format_execution_streams(ExecutionStreams::StdIn), "\n"].join ) end + def write_file_content(file_path, content) + File.write(file_path, content) + end + # Writes required code blocks to a temporary file and sets an environment variable with its path. # # @param mdoc [Object] The Markdown document object. # @param block_name [String] The name of the block to collect code for. def write_required_blocks_to_temp_file(mdoc, block_name) @@ -1656,10 +1628,14 @@ c1).join("\n") create_temp_file_with_code(code_blocks) end + def write_to_file(path, content) + File.write(path, content) + end + # Yields a line as a new block if the selected message type includes :line. # @param [String] line The line to be processed. # @param [Array<Symbol>] selected_messages A list of message types to check. # @param [Proc] block The block to be called with the line data. def yield_line_if_selected(line, selected_messages, &block) @@ -1718,30 +1694,30 @@ # Call method opts_execute_approved_block c.execute_approved_block([], MarkdownExec::FCB.new) end # Test case for empty body - def test_handle_link_block_with_empty_body + def test_push_link_history_and_trigger_load_with_empty_body assert_equal LoadFileNextBlock.new(LoadFile::Reuse, ''), - @hd.handle_link_block([], nil, false) + @hd.push_link_history_and_trigger_load([], nil, false) end # Test case for non-empty body without 'file' key - def test_handle_link_block_without_file_key + def test_push_link_history_and_trigger_load_without_file_key body = ["vars:\n KEY: VALUE"] assert_equal LoadFileNextBlock.new(LoadFile::Reuse, ''), - @hd.handle_link_block(body, nil, false) + @hd.push_link_history_and_trigger_load(body, nil, false) end # Test case for non-empty body with 'file' key - def test_handle_link_block_with_file_key + def test_push_link_history_and_trigger_load_with_file_key body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"] expected_result = LoadFileNextBlock.new(LoadFile::Load, 'sample_block') # mdoc = MDoc.new() assert_equal expected_result, - @hd.handle_link_block(body, nil, FCB.new) + @hd.push_link_history_and_trigger_load(body, nil, FCB.new) end def test_history_env_state_exist_with_value ENV[MDE_HISTORY_ENV_NAME] = 'history_value' assert @hd.history_env_state_exist? @@ -2259,15 +2235,15 @@ def setup @hd = HashDelegator.new @hd.stubs(:history_state_pop) end - def test_handle_back_link + def test_pop_link_history_and_trigger_load # Verifying that history_state_pop is called @hd.expects(:history_state_pop).once - result = @hd.handle_back_link + result = @hd.pop_link_history_and_trigger_load # Asserting the result is an instance of LoadFileNextBlock assert_instance_of LoadFileNextBlock, result assert_equal LoadFile::Load, result.load_file assert_equal '', result.next_block @@ -2318,31 +2294,31 @@ @hd = HashDelegator.new @mock_document = mock('MarkdownDocument') @selected_item = mock('FCB') end - def test_handle_generic_block_without_user_approval + def test_compile_execute_bash_and_special_blocks_and_trigger_reuse_without_user_approval # Mock the delegate object configuration @hd.instance_variable_set(:@delegate_object, { output_script: false, user_must_approve: false }) # Test the method without user approval # Expectations and assertions go here end - def test_handle_generic_block_with_user_approval + def test_compile_execute_bash_and_special_blocks_and_trigger_reuse_with_user_approval # Mock the delegate object configuration @hd.instance_variable_set(:@delegate_object, { output_script: false, user_must_approve: true }) # Test the method with user approval # Expectations and assertions go here end - def test_handle_generic_block_with_output_script + def test_compile_execute_bash_and_special_blocks_and_trigger_reuse_with_output_script # Mock the delegate object configuration @hd.instance_variable_set(:@delegate_object, { output_script: true, user_must_approve: false }) @@ -2359,27 +2335,27 @@ menu_opts_set_color: :blue }) @hd.stubs(:string_send_color) @hd.stubs(:print) end - def test_handle_opts_block + def test_update_options_and_trigger_reuse selected = { body: ['option1: value1'] } tgt2 = {} - result = @hd.handle_opts_block(selected, tgt2) + result = @hd.update_options_and_trigger_reuse(selected, tgt2) assert_instance_of LoadFileNextBlock, result assert_equal 'value1', @hd.instance_variable_get(:@delegate_object)[:option1] assert_equal 'value1', tgt2[:option1] end - def test_handle_opts_block_without_format + def test_update_options_and_trigger_reuse_without_format selected = { body: ['option2: value2'] } @hd.instance_variable_set(:@delegate_object, {}) - result = @hd.handle_opts_block(selected) + result = @hd.update_options_and_trigger_reuse(selected) assert_instance_of LoadFileNextBlock, result assert_equal 'value2', @hd.instance_variable_get(:@delegate_object)[:option2] end @@ -2542,10 +2518,10 @@ class TestHashDelegatorLoadAutoBlocks < Minitest::Test def setup @hd = HashDelegator.new @hd.stubs(:block_find).returns({}) - @hd.stubs(:handle_opts_block) + @hd.stubs(:update_options_and_trigger_reuse) end def test_load_auto_blocks_with_new_filename @hd.instance_variable_set(:@delegate_object, { document_load_opts_block_name: 'load_block',